@motiadev/workbench 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +50 -0
  2. package/components.json +21 -0
  3. package/dist/.empty +0 -0
  4. package/dist/assets/index-DGmArPOa.css +1 -0
  5. package/dist/assets/index-hQsWtfVb.js +182 -0
  6. package/dist/index.html +20 -0
  7. package/eslint.config.js +28 -0
  8. package/index.html +19 -0
  9. package/index.tsx +10 -0
  10. package/middleware.ts +46 -0
  11. package/package.json +56 -0
  12. package/postcss.config.js +6 -0
  13. package/public/.empty +0 -0
  14. package/src/assets/.empty +0 -0
  15. package/src/components/app-sidebar.tsx +55 -0
  16. package/src/components/log-console.tsx +76 -0
  17. package/src/components/log-level-badge.tsx +12 -0
  18. package/src/components/ui/badge.tsx +31 -0
  19. package/src/components/ui/button.tsx +47 -0
  20. package/src/components/ui/collapsible.tsx +9 -0
  21. package/src/components/ui/dialog.tsx +120 -0
  22. package/src/components/ui/input.tsx +21 -0
  23. package/src/components/ui/label.tsx +26 -0
  24. package/src/components/ui/select.tsx +157 -0
  25. package/src/components/ui/separator.tsx +22 -0
  26. package/src/components/ui/sheet.tsx +106 -0
  27. package/src/components/ui/sidebar.tsx +637 -0
  28. package/src/components/ui/skeleton.tsx +7 -0
  29. package/src/components/ui/switch.tsx +27 -0
  30. package/src/components/ui/table.tsx +76 -0
  31. package/src/components/ui/textarea.tsx +22 -0
  32. package/src/components/ui/tooltip.tsx +32 -0
  33. package/src/hooks/use-list-flows.tsx +20 -0
  34. package/src/hooks/use-log-listener.tsx +32 -0
  35. package/src/hooks/use-mobile.tsx +19 -0
  36. package/src/index.css +190 -0
  37. package/src/lib/utils.ts +6 -0
  38. package/src/main.tsx +28 -0
  39. package/src/publicComponents/api-node.tsx +28 -0
  40. package/src/publicComponents/base-handle.tsx +43 -0
  41. package/src/publicComponents/base-node.tsx +57 -0
  42. package/src/publicComponents/emits.tsx +22 -0
  43. package/src/publicComponents/event-node.tsx +36 -0
  44. package/src/publicComponents/node-props.tsx +15 -0
  45. package/src/publicComponents/noop-node.tsx +21 -0
  46. package/src/publicComponents/subscribe.tsx +19 -0
  47. package/src/route-wrapper.tsx +9 -0
  48. package/src/routeTree.gen.ts +109 -0
  49. package/src/routes/__root.tsx +26 -0
  50. package/src/routes/flow/$id.tsx +21 -0
  51. package/src/routes/index.tsx +13 -0
  52. package/src/stores/use-logs.ts +22 -0
  53. package/src/views/flow/arrow-head.tsx +13 -0
  54. package/src/views/flow/base-edge.tsx +44 -0
  55. package/src/views/flow/flow-loader.tsx +3 -0
  56. package/src/views/flow/flow-view.tsx +72 -0
  57. package/src/views/flow/hooks/use-get-flow-state.tsx +109 -0
  58. package/src/views/flow/hooks/use-organize-nodes.ts +60 -0
  59. package/src/views/flow/legend.tsx +59 -0
  60. package/src/views/flow/node-organizer.tsx +70 -0
  61. package/src/views/flow/nodes/api-flow-node.tsx +6 -0
  62. package/src/views/flow/nodes/event-flow-node.tsx +6 -0
  63. package/src/views/flow/nodes/json-schema-form.tsx +110 -0
  64. package/src/views/flow/nodes/language-indicator.tsx +74 -0
  65. package/src/views/flow/nodes/nodes.types.ts +36 -0
  66. package/src/views/flow/nodes/noop-flow-node.tsx +6 -0
  67. package/src/vite-env.d.ts +1 -0
  68. package/tailwind.config.ts +75 -0
  69. package/tsconfig.app.json +30 -0
  70. package/tsconfig.json +13 -0
  71. package/tsconfig.node.json +22 -0
  72. package/tsconfig.node.tsbuildinfo +1 -0
  73. package/vite.config.ts +14 -0
@@ -0,0 +1,9 @@
1
+ import { AppSidebar } from './components/app-sidebar'
2
+ import { SidebarProvider } from './components/ui/sidebar'
3
+
4
+ export const RouteWrapper = ({ children }: { children: React.ReactNode }) => (
5
+ <SidebarProvider>
6
+ <AppSidebar />
7
+ {children}
8
+ </SidebarProvider>
9
+ )
@@ -0,0 +1,109 @@
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ // Import Routes
12
+
13
+ import { Route as rootRoute } from './routes/__root'
14
+ import { Route as IndexImport } from './routes/index'
15
+ import { Route as FlowIdImport } from './routes/flow/$id'
16
+
17
+ // Create/Update Routes
18
+
19
+ const IndexRoute = IndexImport.update({
20
+ id: '/',
21
+ path: '/',
22
+ getParentRoute: () => rootRoute,
23
+ } as any)
24
+
25
+ const FlowIdRoute = FlowIdImport.update({
26
+ id: '/flow/$id',
27
+ path: '/flow/$id',
28
+ getParentRoute: () => rootRoute,
29
+ } as any)
30
+
31
+ // Populate the FileRoutesByPath interface
32
+
33
+ declare module '@tanstack/react-router' {
34
+ interface FileRoutesByPath {
35
+ '/': {
36
+ id: '/'
37
+ path: '/'
38
+ fullPath: '/'
39
+ preLoaderRoute: typeof IndexImport
40
+ parentRoute: typeof rootRoute
41
+ }
42
+ '/flow/$id': {
43
+ id: '/flow/$id'
44
+ path: '/flow/$id'
45
+ fullPath: '/flow/$id'
46
+ preLoaderRoute: typeof FlowIdImport
47
+ parentRoute: typeof rootRoute
48
+ }
49
+ }
50
+ }
51
+
52
+ // Create and export the route tree
53
+
54
+ export interface FileRoutesByFullPath {
55
+ '/': typeof IndexRoute
56
+ '/flow/$id': typeof FlowIdRoute
57
+ }
58
+
59
+ export interface FileRoutesByTo {
60
+ '/': typeof IndexRoute
61
+ '/flow/$id': typeof FlowIdRoute
62
+ }
63
+
64
+ export interface FileRoutesById {
65
+ __root__: typeof rootRoute
66
+ '/': typeof IndexRoute
67
+ '/flow/$id': typeof FlowIdRoute
68
+ }
69
+
70
+ export interface FileRouteTypes {
71
+ fileRoutesByFullPath: FileRoutesByFullPath
72
+ fullPaths: '/' | '/flow/$id'
73
+ fileRoutesByTo: FileRoutesByTo
74
+ to: '/' | '/flow/$id'
75
+ id: '__root__' | '/' | '/flow/$id'
76
+ fileRoutesById: FileRoutesById
77
+ }
78
+
79
+ export interface RootRouteChildren {
80
+ IndexRoute: typeof IndexRoute
81
+ FlowIdRoute: typeof FlowIdRoute
82
+ }
83
+
84
+ const rootRouteChildren: RootRouteChildren = {
85
+ IndexRoute: IndexRoute,
86
+ FlowIdRoute: FlowIdRoute,
87
+ }
88
+
89
+ export const routeTree = rootRoute._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>()
90
+
91
+ /* ROUTE_MANIFEST_START
92
+ {
93
+ "routes": {
94
+ "__root__": {
95
+ "filePath": "__root.tsx",
96
+ "children": [
97
+ "/",
98
+ "/flow/$id"
99
+ ]
100
+ },
101
+ "/": {
102
+ "filePath": "index.tsx"
103
+ },
104
+ "/flow/$id": {
105
+ "filePath": "flow/$id.tsx"
106
+ }
107
+ }
108
+ }
109
+ ROUTE_MANIFEST_END */
@@ -0,0 +1,26 @@
1
+ import { RouteWrapper } from '@/route-wrapper'
2
+ import { createRootRoute, Outlet } from '@tanstack/react-router'
3
+ import React, { Suspense } from 'react'
4
+
5
+ const TanStackRouterDevtools =
6
+ process.env.NODE_ENV === 'production'
7
+ ? () => null // Render nothing in production
8
+ : React.lazy(() =>
9
+ // Lazy load in development
10
+ import('@tanstack/router-devtools').then((res) => ({
11
+ default: res.TanStackRouterDevtools,
12
+ // For Embedded Mode
13
+ // default: res.TanStackRouterDevtoolsPanel
14
+ })),
15
+ )
16
+
17
+ const RouteComponent = () => (
18
+ <RouteWrapper>
19
+ <Outlet />
20
+ <Suspense>
21
+ <TanStackRouterDevtools />
22
+ </Suspense>
23
+ </RouteWrapper>
24
+ )
25
+
26
+ export const Route = createRootRoute({ component: RouteComponent })
@@ -0,0 +1,21 @@
1
+ import { FlowView } from '@/views/flow/flow-view'
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+
4
+ export const Route = createFileRoute('/flow/$id')({
5
+ component: Flow,
6
+ loader: async ({ params }) => {
7
+ return fetch(`/flows/${params.id}`)
8
+ .then((res) => res.json())
9
+ .then((flow) => ({ flow }))
10
+ },
11
+ })
12
+
13
+ function Flow() {
14
+ const { flow } = Route.useLoaderData()
15
+
16
+ return (
17
+ <div className="w-screen h-screen">
18
+ <FlowView flow={flow} />
19
+ </div>
20
+ )
21
+ }
@@ -0,0 +1,13 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ export const Route = createFileRoute('/')({
4
+ component: Index,
5
+ })
6
+
7
+ function Index() {
8
+ return (
9
+ <div className="flex items-center justify-center w-full h-screen">
10
+ <p className="text-gray-500">Select a flow</p>
11
+ </div>
12
+ )
13
+ }
@@ -0,0 +1,22 @@
1
+ import { create } from 'zustand'
2
+
3
+ export type Log = {
4
+ level: string
5
+ time: number
6
+ msg: string
7
+ traceId: string
8
+ flows: string[]
9
+ [key: string]: any
10
+ }
11
+
12
+ export type LogsState = {
13
+ logs: Log[]
14
+ addLog: (log: Log) => void
15
+ resetLogs: () => void
16
+ }
17
+
18
+ export const useLogs = create<LogsState>()((set) => ({
19
+ logs: [],
20
+ addLog: (log) => set((state) => ({ ...state, logs: [...state.logs, log] })),
21
+ resetLogs: () => set({ logs: [] }),
22
+ }))
@@ -0,0 +1,13 @@
1
+ import React from 'react'
2
+
3
+ type Props = {
4
+ color: string
5
+ id: string
6
+ }
7
+
8
+ export const ArrowHead: React.FC<Props> = (props) => (
9
+ <marker id={props.id} viewBox="-5 -5 10 10" markerUnits="strokeWidth" markerWidth="10" markerHeight="10">
10
+ <line x1={0} y1={0} x2={2} y2={-2} stroke={props.color} strokeWidth="1" strokeOpacity="1" strokeLinecap="round" />
11
+ <line x1={-2} y1={-2} x2={0} y2={0} stroke={props.color} strokeWidth="1" strokeOpacity="1" strokeLinecap="round" />
12
+ </marker>
13
+ )
@@ -0,0 +1,44 @@
1
+ import {
2
+ BaseEdge as BaseReactFlowEdge,
3
+ EdgeProps,
4
+ getSmoothStepPath,
5
+ } from '@xyflow/react'
6
+ import React from 'react'
7
+
8
+ export const BaseEdge: React.FC<EdgeProps> = (props) => {
9
+ const {
10
+ sourceX,
11
+ sourceY,
12
+ targetX,
13
+ targetY,
14
+ sourcePosition,
15
+ targetPosition,
16
+ } = props
17
+
18
+ // getSmoothStepPath returns an array: [edgePath, labelX, labelY, etc.]
19
+ // We just need edgePath here.
20
+ const [edgePath] = getSmoothStepPath({
21
+ sourceX,
22
+ sourceY,
23
+ targetX,
24
+ targetY,
25
+ sourcePosition,
26
+ targetPosition,
27
+ borderRadius: 20, // <— Tweak this for roundness
28
+ offset: 10, // <— How far the line extends before curving
29
+ })
30
+
31
+ return (
32
+ <BaseReactFlowEdge
33
+ path={edgePath}
34
+ style={{
35
+ stroke: 'rgba(100, 100, 100)',
36
+ strokeWidth: 0.5,
37
+ shapeRendering: 'geometricPrecision',
38
+ fill: 'none',
39
+ mixBlendMode: 'screen', // or 'screen'
40
+ }}
41
+ className="edge-animated"
42
+ />
43
+ )
44
+ }
@@ -0,0 +1,3 @@
1
+ export const FlowLoader = () => {
2
+ return <div className="absolute z-10 inset-0 w-full h-full bg-[#060911]" />
3
+ }
@@ -0,0 +1,72 @@
1
+ import { Background, BackgroundVariant, ReactFlow, Node } from '@xyflow/react'
2
+ import '@xyflow/react/dist/style.css'
3
+ import { BaseEdge } from './base-edge'
4
+ import { ArrowHead } from './arrow-head'
5
+ import { useGetFlowState, FlowResponse } from './hooks/use-get-flow-state'
6
+ import { useCallback, useEffect, useState } from 'react'
7
+ import { NodeOrganizer } from './node-organizer'
8
+ import { FlowLoader } from './flow-loader'
9
+ import { useLogListener } from '@/hooks/use-log-listener'
10
+ import { LogConsole } from '@/components/log-console'
11
+ import { Legend } from './legend'
12
+
13
+ const edgeTypes = {
14
+ base: BaseEdge,
15
+ }
16
+
17
+ type Props = {
18
+ flow: FlowResponse
19
+ }
20
+
21
+ export const FlowView: React.FC<Props> = ({ flow }) => {
22
+ const { nodes, edges, onNodesChange, onEdgesChange, nodeTypes } = useGetFlowState(flow)
23
+ const [initialized, setInitialized] = useState(false)
24
+ const [hoveredType, setHoveredType] = useState<string | null>(null)
25
+
26
+ useLogListener()
27
+
28
+ useEffect(() => setInitialized(false), [flow])
29
+ const onInitialized = useCallback(() => {
30
+ setTimeout(() => setInitialized(true), 10)
31
+ }, [])
32
+
33
+ const highlightClass = (nodeType?: string) => {
34
+ if (!hoveredType) return ''
35
+ return nodeType === hoveredType
36
+ ? 'shadow-[0_0_15px_rgba(255,255,255,0.15)] border border-white/30 scale-[1.02] transition-all duration-300'
37
+ : 'opacity-30 transition-all duration-300'
38
+ }
39
+
40
+ const nodesWithHighlights = nodes.map((node) => ({
41
+ ...node,
42
+ className: highlightClass(node.data.type), // Access type from `data.type`
43
+ }))
44
+
45
+ if (!nodeTypes) {
46
+ return null
47
+ }
48
+
49
+ return (
50
+ <div className="w-full h-full relative bg-black">
51
+ {!initialized && <FlowLoader />}
52
+ <Legend onHover={setHoveredType} />
53
+ <ReactFlow
54
+ nodes={nodesWithHighlights}
55
+ edges={edges}
56
+ nodeTypes={nodeTypes}
57
+ edgeTypes={edgeTypes}
58
+ onNodesChange={onNodesChange}
59
+ onEdgesChange={onEdgesChange}
60
+ >
61
+ <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#444" />
62
+ <NodeOrganizer onInitialized={onInitialized} />
63
+ <svg>
64
+ <defs>
65
+ <ArrowHead color="#B3B3B3" id="arrowhead" />
66
+ </defs>
67
+ </svg>
68
+ </ReactFlow>
69
+ <LogConsole />
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,109 @@
1
+ import { Edge, Node, useEdgesState, useNodesState } from '@xyflow/react'
2
+ import { useEffect, useState } from 'react'
3
+ import type { EdgeData, NodeData } from '../nodes/nodes.types'
4
+ import { ApiFlowNode } from '../nodes/api-flow-node'
5
+ import { NoopFlowNode } from '../nodes/noop-flow-node'
6
+ import { EventFlowNode } from '../nodes/event-flow-node'
7
+
8
+ type Emit = string | { type: string; label?: string; conditional?: boolean }
9
+
10
+ type FlowStep = {
11
+ id: string
12
+ name: string
13
+ type: 'event' | 'api' | 'noop'
14
+ description?: string
15
+ subscribes?: string[]
16
+ emits: Emit[]
17
+ action: 'webhook'
18
+ webhookUrl?: string
19
+ language?: string
20
+ nodeComponentPath?: string
21
+ }
22
+
23
+ export type FlowResponse = {
24
+ id: string
25
+ name: string
26
+ steps: FlowStep[]
27
+ }
28
+
29
+ type FlowState = {
30
+ nodes: Node<NodeData>[]
31
+ edges: Edge<EdgeData>[]
32
+ nodeTypes: Record<string, React.ComponentType<any>>
33
+ }
34
+
35
+ async function importFlow(flow: FlowResponse): Promise<FlowState> {
36
+ const nodeTypes: Record<string, React.ComponentType<any>> = {
37
+ event: EventFlowNode,
38
+ api: ApiFlowNode,
39
+ noop: NoopFlowNode,
40
+ }
41
+
42
+ for (const step of flow.steps) {
43
+ if (step.nodeComponentPath) {
44
+ const module = await import(/* @vite-ignore */ step.nodeComponentPath)
45
+ nodeTypes[step.nodeComponentPath] = module.default
46
+ }
47
+ }
48
+
49
+ // we need to check all subscribes and emits to connect the nodes using edges
50
+ const nodes: Node<NodeData>[] = flow.steps.map((step) => ({
51
+ id: step.id,
52
+ type: step.nodeComponentPath ? step.nodeComponentPath : step.type,
53
+ position: { x: 0, y: 0 },
54
+ data: step,
55
+ language: step.language,
56
+ }))
57
+
58
+ const edges: Edge<EdgeData>[] = []
59
+
60
+ // For each node that emits events
61
+ flow.steps.forEach((sourceNode) => {
62
+ const emits = sourceNode.emits || []
63
+
64
+ // Check all other nodes that subscribe to those events
65
+ flow.steps.forEach((targetNode) => {
66
+ const subscribes = targetNode.subscribes || []
67
+
68
+ // For each matching emit->subscribe, create an edge
69
+ emits.forEach((emit) => {
70
+ const emitType = typeof emit === 'string' ? emit : emit.type
71
+
72
+ if (subscribes.includes(emitType)) {
73
+ const label = typeof emit !== 'string' ? emit.label : undefined
74
+ const variant = typeof emit !== 'string' && emit.conditional ? 'conditional' : 'default'
75
+ const data: EdgeData = { variant, label }
76
+
77
+ edges.push({
78
+ id: `${sourceNode.id}-${targetNode.id}`,
79
+ type: 'base',
80
+ source: sourceNode.id,
81
+ target: targetNode.id,
82
+ label,
83
+ data,
84
+ })
85
+ }
86
+ })
87
+ })
88
+ })
89
+
90
+ return { nodes, edges, nodeTypes }
91
+ }
92
+
93
+ export const useGetFlowState = (flow: FlowResponse) => {
94
+ const [nodeTypes, setNodeTypes] = useState<Record<string, React.ComponentType<any>>>()
95
+ const [nodes, setNodes, onNodesChange] = useNodesState<Node<NodeData>>([])
96
+ const [edges, setEdges, onEdgesChange] = useEdgesState<Edge<EdgeData>>([])
97
+
98
+ useEffect(() => {
99
+ if (!flow) return
100
+
101
+ importFlow(flow).then(({ nodes, edges, nodeTypes }) => {
102
+ setNodes(nodes)
103
+ setEdges(edges)
104
+ setNodeTypes(nodeTypes)
105
+ })
106
+ }, [flow])
107
+
108
+ return { nodes, edges, onNodesChange, onEdgesChange, nodeTypes }
109
+ }
@@ -0,0 +1,60 @@
1
+ import { Edge, Node } from '@xyflow/react'
2
+ import dagre from 'dagre'
3
+ import { useEffect, useRef } from 'react'
4
+ import { EventNodeData, EdgeData, ApiNodeData } from '../nodes/nodes.types'
5
+
6
+ const organizeNodes = (
7
+ nodes: Node<EventNodeData | ApiNodeData>[],
8
+ edges: Edge<EdgeData>[],
9
+ ): Node<EventNodeData | ApiNodeData>[] => {
10
+ const dagreGraph = new dagre.graphlib.Graph({ compound: true })
11
+ dagreGraph.setDefaultEdgeLabel(() => ({}))
12
+
13
+ // Top-to-bottom layout
14
+ dagreGraph.setGraph({ rankdir: 'TB', ranksep: 80, nodesep: 60, edgesep: 20, ranker: 'tight-tree' })
15
+
16
+ nodes.forEach((node) => {
17
+ dagreGraph.setNode(node.id, { width: node.measured?.width, height: node.measured?.height })
18
+ })
19
+
20
+ edges.forEach((edge) => {
21
+ if (typeof edge.label === 'string') {
22
+ dagreGraph.setEdge(edge.source, edge.target, {
23
+ label: edge.label ?? '',
24
+ width: edge.label.length * 40, // Add width for the label
25
+ height: 30, // Add height for the label
26
+ labelpos: 'c', // Position label in center
27
+ })
28
+ } else {
29
+ dagreGraph.setEdge(edge.source, edge.target)
30
+ }
31
+ })
32
+
33
+ dagre.layout(dagreGraph)
34
+
35
+ return nodes.map((node) => {
36
+ const { x, y } = dagreGraph.node(node.id)
37
+ const position = {
38
+ x: x - (node.measured?.width ?? 0) / 2,
39
+ y: y - (node.measured?.height ?? 0) / 2,
40
+ }
41
+
42
+ return { ...node, position }
43
+ })
44
+ }
45
+
46
+ export const useOrganizeNodes = (
47
+ nodes: Node<EventNodeData | ApiNodeData>[],
48
+ edges: Edge<EdgeData>[],
49
+ setNodes: (nodes: Node<EventNodeData | ApiNodeData>[]) => void,
50
+ ) => {
51
+ const organizedRef = useRef<boolean>(false)
52
+
53
+ useEffect(() => {
54
+ if (!nodes.length || !edges.length || !nodes[0].measured || organizedRef.current) return
55
+
56
+ const layoutedNodes = organizeNodes(nodes, edges)
57
+ setNodes(layoutedNodes)
58
+ organizedRef.current = true
59
+ }, [nodes, edges, setNodes])
60
+ }
@@ -0,0 +1,59 @@
1
+ export const Legend = ({ onHover }: { onHover: (type: string | null) => void }) => {
2
+ const legendItems = [
3
+ {
4
+ label: 'Event (Core)',
5
+ type: 'event',
6
+ bgColor: 'bg-green-950/40',
7
+ description: 'Core logic components that process events',
8
+ },
9
+ {
10
+ label: 'API',
11
+ type: 'api',
12
+ bgColor: 'bg-blue-950/40',
13
+ description: 'HTTP endpoints that trigger flows',
14
+ },
15
+ {
16
+ label: 'Noop (Non-Operation)',
17
+ type: 'noop',
18
+ bgColor: 'bg-zinc-950/40',
19
+ description: 'Placeholder nodes for external processes',
20
+ },
21
+ ]
22
+
23
+ const renderSwatch = (bgColor: string) => (
24
+ <div className="relative group">
25
+ {/* Border gradient container */}
26
+ <div className="absolute -inset-[1px] rounded bg-gradient-to-r from-white/20 to-white/10" />
27
+
28
+ {/* Main swatch */}
29
+ <div className={`relative ${bgColor} w-8 h-8 rounded border border-white/10`} />
30
+
31
+ {/* Stacked effect */}
32
+ <div className="absolute inset-0 -z-10 translate-y-0.5 translate-x-0.5 bg-black/20 rounded border border-white/5" />
33
+ </div>
34
+ )
35
+
36
+ return (
37
+ <div className="absolute right-4 top-4 font-mono rounded-lg border border-white/20 p-4 z-10 shadow-xl">
38
+ <div className="text-sm text-white mb-3 font-semibold">Flow Legend</div>
39
+ <div className="flex flex-col gap-3">
40
+ {legendItems.map((item) => (
41
+ <div
42
+ key={item.type}
43
+ onMouseEnter={() => onHover(item.type)}
44
+ onMouseLeave={() => onHover(null)}
45
+ className="group cursor-pointer transition-all hover:bg-white/5 rounded p-1 -mx-1"
46
+ >
47
+ <div className="flex items-start gap-3">
48
+ {renderSwatch(item.bgColor)}
49
+ <div className="flex-1">
50
+ <div className="text-white text-xs font-medium">{item.label}</div>
51
+ <div className="text-white/60 text-xs mt-0.5">{item.description}</div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,70 @@
1
+ import { Edge, Node, useNodesInitialized, useReactFlow } from '@xyflow/react'
2
+ import dagre from 'dagre'
3
+ import { useEffect } from 'react'
4
+ import { EventNodeData, EdgeData, ApiNodeData } from './nodes/nodes.types'
5
+
6
+ const organizeNodes = (
7
+ nodes: Node<EventNodeData | ApiNodeData>[],
8
+ edges: Edge<EdgeData>[],
9
+ ): Node<EventNodeData | ApiNodeData>[] => {
10
+ const dagreGraph = new dagre.graphlib.Graph({ compound: true })
11
+ dagreGraph.setDefaultEdgeLabel(() => ({}))
12
+
13
+ // Top-to-bottom layout
14
+ dagreGraph.setGraph({ rankdir: 'TB', ranksep: 80, nodesep: 60, edgesep: 20, ranker: 'tight-tree' })
15
+
16
+ nodes.forEach((node) => {
17
+ dagreGraph.setNode(node.id, { width: node.measured?.width, height: node.measured?.height })
18
+ })
19
+
20
+ edges.forEach((edge) => {
21
+ if (typeof edge.label === 'string') {
22
+ dagreGraph.setEdge(edge.source, edge.target, {
23
+ label: edge.label ?? '',
24
+ width: edge.label.length * 40, // Add width for the label
25
+ height: 30, // Add height for the label
26
+ labelpos: 'c', // Position label in center
27
+ })
28
+ } else {
29
+ dagreGraph.setEdge(edge.source, edge.target)
30
+ }
31
+ })
32
+
33
+ dagre.layout(dagreGraph)
34
+
35
+ return nodes.map((node) => {
36
+ const { x, y } = dagreGraph.node(node.id)
37
+ const position = {
38
+ x: x - (node.measured?.width ?? 0) / 2,
39
+ y: y - (node.measured?.height ?? 0) / 2,
40
+ }
41
+
42
+ return { ...node, position }
43
+ })
44
+ }
45
+
46
+ type Props = {
47
+ onInitialized: () => void
48
+ }
49
+
50
+ export const NodeOrganizer: React.FC<Props> = ({ onInitialized }) => {
51
+ const { setNodes, getNodes, getEdges, fitView } = useReactFlow()
52
+ const nodesInitialized = useNodesInitialized()
53
+
54
+ useEffect(() => {
55
+ if (nodesInitialized) {
56
+ const nodes = getNodes() as Node<EventNodeData | ApiNodeData>[]
57
+ const edges = getEdges() as Edge<EdgeData>[]
58
+ const organizedNodes = organizeNodes(nodes, edges)
59
+
60
+ setNodes(organizedNodes)
61
+ onInitialized()
62
+
63
+ setTimeout(async () => {
64
+ await fitView()
65
+ }, 1)
66
+ }
67
+ }, [nodesInitialized, onInitialized])
68
+
69
+ return null
70
+ }
@@ -0,0 +1,6 @@
1
+ import { ApiNode } from '../../../publicComponents/api-node'
2
+ import { ApiNodeData } from './nodes.types'
3
+
4
+ export const ApiFlowNode = ({ data }: { data: ApiNodeData }) => {
5
+ return <ApiNode data={data} />
6
+ }
@@ -0,0 +1,6 @@
1
+ import { EventNode } from '../../../publicComponents/event-node'
2
+ import { EventNodeProps } from '../../../publicComponents/node-props'
3
+
4
+ export const EventFlowNode = ({ data }: EventNodeProps) => {
5
+ return <EventNode className="relative" data={data}></EventNode>
6
+ }