@majdibo/flow-visualizer 1.0.0

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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Generic modal for displaying node details
3
+ */
4
+
5
+ import type { VisualNode } from './FlowVisualizer';
6
+ import type { FlowGraph } from '@majdibo/flow-engine';
7
+ import type { VisualState } from '@majdibo/flow-engine';
8
+ import './modal.css';
9
+
10
+ /** Trace data structure */
11
+ export interface TraceData {
12
+ step: string;
13
+ nodeType: 'action' | 'decision';
14
+ data: any;
15
+ timestamp: number;
16
+ }
17
+
18
+ /** Modal data passed to consumers */
19
+ export interface NodeDetailModalData<T extends TraceData = TraceData> {
20
+ nodeId: string;
21
+ node: VisualNode | undefined;
22
+ graphNode: any;
23
+ nodeTraces: T[];
24
+ allTraces: T[];
25
+ visualState: VisualState;
26
+ }
27
+
28
+ interface NodeDetailModalProps<T extends TraceData = TraceData> {
29
+ nodeId: string;
30
+ nodes: VisualNode[];
31
+ graph: FlowGraph;
32
+ traces: T[];
33
+ visualState: VisualState;
34
+ onClose: () => void;
35
+ children: React.ReactNode;
36
+ }
37
+
38
+ /**
39
+ * Modal wrapper with semantic CSS classes
40
+ *
41
+ * Provides overlay, backdrop, and container structure.
42
+ * Consumers provide custom content as children.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * <NodeDetailModal {...props}>
47
+ * <YourCustomContent />
48
+ * </NodeDetailModal>
49
+ * ```
50
+ */
51
+ export function NodeDetailModal<T extends TraceData = TraceData>({
52
+ onClose,
53
+ children
54
+ }: NodeDetailModalProps<T>) {
55
+ return (
56
+ <div className="flow-modal-overlay" onClick={onClose}>
57
+ <div className="flow-modal-backdrop" />
58
+ <div className="flow-modal-container" onClick={(e) => e.stopPropagation()}>
59
+ {children}
60
+ </div>
61
+ </div>
62
+ );
63
+ }
package/src/index.css ADDED
@@ -0,0 +1,119 @@
1
+ @import "tailwindcss";
2
+
3
+ @layer base {
4
+ body {
5
+ margin: 0;
6
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
7
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
8
+ sans-serif;
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ }
12
+
13
+ code {
14
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
15
+ monospace;
16
+ }
17
+ }
18
+
19
+ /* Custom animations for smooth transitions */
20
+ @layer utilities {
21
+ .animate-in {
22
+ animation-duration: 200ms;
23
+ animation-timing-function: ease-out;
24
+ }
25
+
26
+ .fade-in {
27
+ animation-name: fadeIn;
28
+ }
29
+
30
+ .zoom-in {
31
+ animation-name: zoomIn;
32
+ }
33
+
34
+ @keyframes fadeIn {
35
+ from {
36
+ opacity: 0;
37
+ }
38
+ to {
39
+ opacity: 1;
40
+ }
41
+ }
42
+
43
+ @keyframes zoomIn {
44
+ from {
45
+ opacity: 0;
46
+ transform: scale(0.95);
47
+ }
48
+ to {
49
+ opacity: 1;
50
+ transform: scale(1);
51
+ }
52
+ }
53
+
54
+ .scrollbar-thin::-webkit-scrollbar {
55
+ width: 6px;
56
+ height: 6px;
57
+ }
58
+
59
+ .scrollbar-thin::-webkit-scrollbar-track {
60
+ background: transparent;
61
+ }
62
+
63
+ .scrollbar-thin::-webkit-scrollbar-thumb {
64
+ background: rgb(209 213 219);
65
+ border-radius: 3px;
66
+ }
67
+
68
+ .scrollbar-thin::-webkit-scrollbar-thumb:hover {
69
+ background: rgb(156 163 175);
70
+ }
71
+
72
+ /* Slider styling */
73
+ input[type="range"].slider {
74
+ -webkit-appearance: none;
75
+ appearance: none;
76
+ }
77
+
78
+ input[type="range"].slider-gradient {
79
+ background: linear-gradient(
80
+ to right,
81
+ rgb(99 102 241) 0%,
82
+ rgb(99 102 241) var(--slider-position, 0%),
83
+ rgb(229 231 235) var(--slider-position, 0%),
84
+ rgb(229 231 235) 100%
85
+ );
86
+ }
87
+
88
+ input[type="range"].slider::-webkit-slider-thumb {
89
+ -webkit-appearance: none;
90
+ appearance: none;
91
+ width: 18px;
92
+ height: 18px;
93
+ border-radius: 50%;
94
+ background: rgb(99 102 241);
95
+ cursor: pointer;
96
+ border: 2px solid white;
97
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
98
+ transition: transform 0.15s ease;
99
+ }
100
+
101
+ input[type="range"].slider::-webkit-slider-thumb:hover {
102
+ transform: scale(1.15);
103
+ }
104
+
105
+ input[type="range"].slider::-moz-range-thumb {
106
+ width: 18px;
107
+ height: 18px;
108
+ border-radius: 50%;
109
+ background: rgb(99 102 241);
110
+ cursor: pointer;
111
+ border: 2px solid white;
112
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
113
+ transition: transform 0.15s ease;
114
+ }
115
+
116
+ input[type="range"].slider::-moz-range-thumb:hover {
117
+ transform: scale(1.15);
118
+ }
119
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Flow Visualizer - React components for flow visualization
3
+ *
4
+ * @example
5
+ * ```tsx
6
+ * import { FlowVisualizer, computeLayout } from '@agent/flow-visualizer';
7
+ *
8
+ * const { nodes, edges } = computeLayout(graph);
9
+ *
10
+ * <FlowVisualizer
11
+ * nodes={nodes}
12
+ * edges={edges}
13
+ * state={visualState}
14
+ * onNodeClick={(id) => console.log(id)}
15
+ * />
16
+ * ```
17
+ */
18
+
19
+ export { default as FlowVisualizer } from './FlowVisualizer';
20
+ export type { VisualNode, VisualEdge } from './FlowVisualizer';
21
+
22
+ export { NodeDetailModal } from './NodeDetailModal';
23
+ export type { TraceData, NodeDetailModalData } from './NodeDetailModal';
24
+
25
+ export { computeLayout } from './layoutEngine';
26
+ export type { LayoutOptions, LayoutResult } from './layoutEngine';
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Automatic graph layout using Dagre
3
+ */
4
+
5
+ import dagre from 'dagre';
6
+ import type { FlowGraph } from '@majdibo/flow-engine';
7
+ import type { VisualNode, VisualEdge } from './FlowVisualizer';
8
+
9
+ /** Layout configuration options */
10
+ export interface LayoutOptions {
11
+ /** Layout direction: TB (top-bottom), LR (left-right), BT, RL */
12
+ direction?: 'TB' | 'BT' | 'LR' | 'RL';
13
+ /** Horizontal spacing between nodes */
14
+ nodeSep?: number;
15
+ /** Vertical spacing between ranks */
16
+ rankSep?: number;
17
+ /** Node dimensions by type */
18
+ nodeSize?: {
19
+ action: { width: number; height: number };
20
+ decision: { width: number; height: number };
21
+ };
22
+ }
23
+
24
+ /** Layout computation result */
25
+ export interface LayoutResult {
26
+ nodes: VisualNode[];
27
+ edges: VisualEdge[];
28
+ }
29
+
30
+ const DEFAULT_OPTIONS: Required<LayoutOptions> = {
31
+ direction: 'TB',
32
+ nodeSep: 80,
33
+ rankSep: 120,
34
+ nodeSize: {
35
+ action: { width: 140, height: 50 },
36
+ decision: { width: 120, height: 60 },
37
+ },
38
+ };
39
+
40
+ /**
41
+ * Compute automatic layout for a flow graph
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const { nodes, edges } = computeLayout(graph, {
46
+ * direction: 'TB',
47
+ * nodeSep: 80,
48
+ * rankSep: 120
49
+ * });
50
+ * ```
51
+ */
52
+ export function computeLayout(graph: FlowGraph, options?: LayoutOptions): LayoutResult {
53
+ const opts: Required<LayoutOptions> = {
54
+ ...DEFAULT_OPTIONS,
55
+ ...options,
56
+ nodeSize: { ...DEFAULT_OPTIONS.nodeSize, ...options?.nodeSize }
57
+ };
58
+
59
+ const g = new dagre.graphlib.Graph({ multigraph: true });
60
+ g.setGraph({
61
+ rankdir: opts.direction,
62
+ nodesep: opts.nodeSep,
63
+ ranksep: opts.rankSep,
64
+ marginx: 50,
65
+ marginy: 50
66
+ });
67
+ g.setDefaultEdgeLabel(() => ({}));
68
+
69
+ for (const node of graph.nodes) {
70
+ const size = opts.nodeSize[node.type];
71
+ g.setNode(node.id, {
72
+ width: size.width,
73
+ height: size.height,
74
+ label: node.label || node.id,
75
+ type: node.type
76
+ });
77
+ }
78
+
79
+ for (const edge of graph.edges) {
80
+ g.setEdge(edge.from, edge.to, { id: edge.id, label: edge.label }, edge.id);
81
+ }
82
+
83
+ dagre.layout(g);
84
+
85
+ return {
86
+ nodes: convertNodesToVisual(g, graph),
87
+ edges: convertEdgesToVisual(g, graph),
88
+ };
89
+ }
90
+
91
+ function convertNodesToVisual(g: dagre.graphlib.Graph, graph: FlowGraph): VisualNode[] {
92
+ return graph.nodes.map(node => {
93
+ const dagreNode = g.node(node.id);
94
+ if (!dagreNode) {
95
+ console.warn(`Node ${node.id} not found in layout result`);
96
+ return null;
97
+ }
98
+ return {
99
+ id: node.id,
100
+ label: node.label || node.id,
101
+ type: node.type,
102
+ x: dagreNode.x - dagreNode.width / 2,
103
+ y: dagreNode.y - dagreNode.height / 2,
104
+ width: dagreNode.width,
105
+ height: dagreNode.height,
106
+ };
107
+ }).filter(Boolean) as VisualNode[];
108
+ }
109
+
110
+ function convertEdgesToVisual(g: dagre.graphlib.Graph, graph: FlowGraph): VisualEdge[] {
111
+ return graph.edges.map(edge => {
112
+ const dagreEdge = g.edge(edge.from, edge.to, edge.id);
113
+ if (!dagreEdge) {
114
+ console.warn(`Edge ${edge.id} not found`);
115
+ return null;
116
+ }
117
+
118
+ const path = generateSVGPath(dagreEdge.points);
119
+ const midIdx = Math.floor(dagreEdge.points.length / 2);
120
+ const midPoint = dagreEdge.points[midIdx] || { x: 0, y: 0 };
121
+
122
+ return {
123
+ id: edge.id,
124
+ from: edge.from,
125
+ to: edge.to,
126
+ path,
127
+ label: edge.label,
128
+ labelX: midPoint.x,
129
+ labelY: midPoint.y,
130
+ };
131
+ }).filter(Boolean) as VisualEdge[];
132
+ }
133
+
134
+ function generateSVGPath(points: dagre.GraphEdge['points']): string {
135
+ if (!points || points.length === 0) return '';
136
+ let path = `M${points[0].x},${points[0].y}`;
137
+ for (let i = 1; i < points.length; i++) {
138
+ path += ` L${points[i].x},${points[i].y}`;
139
+ }
140
+ return path;
141
+ }
package/src/modal.css ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Flow Modal Base Styles
3
+ * These provide minimal positioning and structure.
4
+ * Consumers should override these classes for custom styling.
5
+ */
6
+
7
+ .flow-modal-overlay {
8
+ position: fixed;
9
+ top: 0;
10
+ left: 0;
11
+ width: 100vw;
12
+ height: 100vh;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ z-index: 9999;
17
+ }
18
+
19
+ .flow-modal-backdrop {
20
+ position: absolute;
21
+ top: 0;
22
+ left: 0;
23
+ width: 100%;
24
+ height: 100%;
25
+ z-index: 1;
26
+ background-color: rgba(0, 0, 0, 0.5);
27
+ backdrop-filter: blur(4px);
28
+ }
29
+
30
+ .flow-modal-container {
31
+ position: relative;
32
+ z-index: 2;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ padding: 1rem;
37
+ max-width: 100%;
38
+ max-height: 100%;
39
+ }
40
+
41
+ .flow-modal-container > div {
42
+ background: white;
43
+ border-radius: 0.75rem;
44
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
45
+ max-width: 600px;
46
+ width: 100%;
47
+ max-height: 80vh;
48
+ overflow-y: auto;
49
+ }
50
+
51
+ @media (prefers-color-scheme: dark) {
52
+ .flow-modal-container > div {
53
+ background: rgb(15 23 42);
54
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
55
+ }
56
+ }
57
+
58
+ .flow-modal-content {
59
+ display: flex;
60
+ flex-direction: column;
61
+ max-height: 80vh;
62
+ overflow: hidden;
63
+ }
64
+
65
+ .flow-modal-header {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: space-between;
69
+ }
70
+
71
+ .flow-modal-title {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 0.75rem;
75
+ }
76
+
77
+ .flow-modal-close {
78
+ cursor: pointer;
79
+ border: none;
80
+ background: none;
81
+ font-size: 1.5rem;
82
+ line-height: 1;
83
+ padding: 0.5rem;
84
+ }
85
+
86
+ .flow-modal-body {
87
+ flex: 1;
88
+ overflow-y: auto;
89
+ }
90
+
91
+ .flow-modal-section {
92
+ margin-bottom: 1rem;
93
+ }
94
+
95
+ .flow-modal-section-title {
96
+ font-weight: 600;
97
+ margin-bottom: 0.5rem;
98
+ }
99
+
100
+ .flow-modal-section-content {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 0.5rem;
104
+ }
105
+
106
+ .flow-modal-state-indicator {
107
+ width: 0.5rem;
108
+ height: 0.5rem;
109
+ border-radius: 50%;
110
+ display: inline-block;
111
+ }
112
+
113
+ .flow-modal-traces {
114
+ display: flex;
115
+ flex-direction: column;
116
+ gap: 0.75rem;
117
+ }
118
+
119
+ .flow-modal-trace {
120
+ border: 1px solid #e5e7eb;
121
+ border-radius: 0.5rem;
122
+ padding: 0.75rem;
123
+ }
124
+
125
+ .flow-modal-trace-header {
126
+ display: flex;
127
+ justify-content: space-between;
128
+ margin-bottom: 0.5rem;
129
+ font-size: 0.875rem;
130
+ }
131
+
132
+ .flow-modal-trace-content {
133
+ margin-bottom: 0.5rem;
134
+ }
135
+
136
+ .flow-modal-trace-details summary {
137
+ cursor: pointer;
138
+ font-size: 0.875rem;
139
+ }
140
+
141
+ .flow-modal-trace-data {
142
+ margin-top: 0.5rem;
143
+ padding: 0.5rem;
144
+ font-size: 0.75rem;
145
+ overflow-x: auto;
146
+ border-radius: 0.25rem;
147
+ }
148
+
149
+ .flow-modal-empty {
150
+ text-align: center;
151
+ padding: 2rem;
152
+ color: #9ca3af;
153
+ }