@aiready/visualizer 0.1.13 → 0.1.18

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,192 @@
1
+ import { useEffect, useState, useMemo } from 'react';
2
+ import { GraphData, FileNode, SeverityLevel, EdgeType } from './types';
3
+ import { themeConfig } from './constants';
4
+ import { transformReportToGraph, loadReportData } from './utils';
5
+ import { useDimensions } from './hooks/useDimensions';
6
+ import { useTheme } from './hooks/useTheme';
7
+ import {
8
+ LoadingSpinner,
9
+ ErrorDisplay,
10
+ Navbar,
11
+ LegendPanel,
12
+ NodeDetails,
13
+ GraphCanvas,
14
+ } from './components';
15
+
16
+ // All available severity levels
17
+ const ALL_SEVERITIES: SeverityLevel[] = ['critical', 'major', 'minor', 'info'];
18
+
19
+ // All available edge types (excluding 'default' and 'reference' as they're filtered out in UI)
20
+ const ALL_EDGE_TYPES: EdgeType[] = ['similarity', 'dependency', 'related'];
21
+
22
+ function App() {
23
+ const [data, setData] = useState<GraphData | null>(null);
24
+ const [selectedNode, setSelectedNode] = useState<FileNode | null>(null);
25
+ const [loading, setLoading] = useState(true);
26
+ const [error, setError] = useState<string | null>(null);
27
+
28
+ // Filter state - start with all visible
29
+ const [visibleSeverities, setVisibleSeverities] = useState<Set<SeverityLevel>>(
30
+ new Set(ALL_SEVERITIES)
31
+ );
32
+ const [visibleEdgeTypes, setVisibleEdgeTypes] = useState<Set<EdgeType>>(
33
+ new Set(ALL_EDGE_TYPES)
34
+ );
35
+
36
+ const { containerRef, dimensions } = useDimensions();
37
+ const { theme, setTheme, effectiveTheme } = useTheme();
38
+
39
+ const colors = themeConfig[effectiveTheme];
40
+
41
+ // Load report data
42
+ useEffect(() => {
43
+ const loadData = async () => {
44
+ const reportData = await loadReportData();
45
+
46
+ if (!reportData) {
47
+ setError(
48
+ 'No scan data found. Run "pnpm aiready scan ." then copy to public/report-data.json'
49
+ );
50
+ setLoading(false);
51
+ return;
52
+ }
53
+
54
+ setData(transformReportToGraph(reportData));
55
+ setLoading(false);
56
+ };
57
+
58
+ loadData();
59
+ }, []);
60
+
61
+ // Toggle severity visibility
62
+ const handleToggleSeverity = (severity: SeverityLevel) => {
63
+ setVisibleSeverities(prev => {
64
+ const next = new Set(prev);
65
+ if (next.has(severity)) {
66
+ next.delete(severity);
67
+ } else {
68
+ next.add(severity);
69
+ }
70
+ return next;
71
+ });
72
+ };
73
+
74
+ // Toggle edge type visibility
75
+ const handleToggleEdgeType = (edgeType: EdgeType) => {
76
+ setVisibleEdgeTypes(prev => {
77
+ const next = new Set(prev);
78
+ if (next.has(edgeType)) {
79
+ next.delete(edgeType);
80
+ } else {
81
+ next.add(edgeType);
82
+ }
83
+ return next;
84
+ });
85
+ };
86
+
87
+ // Filter data based on visible severities and edge types
88
+ // Also hides edges connected to hidden nodes
89
+ const filteredData = useMemo(() => {
90
+ if (!data) return null;
91
+
92
+ // Get set of visible node IDs
93
+ const visibleNodeIds = new Set(
94
+ data.nodes
95
+ .filter(node => {
96
+ const severity = (node.severity || 'default') as SeverityLevel;
97
+ return visibleSeverities.has(severity);
98
+ })
99
+ .map(node => node.id)
100
+ );
101
+
102
+ // Filter nodes: keep if severity is visible
103
+ const filteredNodes = data.nodes.filter(node => {
104
+ const severity = (node.severity || 'default') as SeverityLevel;
105
+ return visibleSeverities.has(severity);
106
+ });
107
+
108
+ // Filter edges: keep if:
109
+ // 1. Edge type is visible AND
110
+ // 2. Both source and target nodes are visible
111
+ const filteredEdges = data.edges.filter(edge => {
112
+ // Check edge type visibility
113
+ const edgeType = (edge.type || 'default') as EdgeType;
114
+ if (!visibleEdgeTypes.has(edgeType)) {
115
+ return false;
116
+ }
117
+ // Check that both connected nodes are visible
118
+ const sourceId = typeof edge.source === 'string' ? edge.source : edge.source.id;
119
+ const targetId = typeof edge.target === 'string' ? edge.target : edge.target.id;
120
+ return visibleNodeIds.has(sourceId) && visibleNodeIds.has(targetId);
121
+ });
122
+
123
+ return {
124
+ nodes: filteredNodes,
125
+ edges: filteredEdges,
126
+ };
127
+ }, [data, visibleSeverities, visibleEdgeTypes]);
128
+
129
+ // Handle loading state
130
+ if (loading) {
131
+ return <LoadingSpinner colors={colors} />;
132
+ }
133
+
134
+ // Handle error state
135
+ if (error) {
136
+ return <ErrorDisplay colors={colors} error={error} />;
137
+ }
138
+
139
+ return (
140
+ <div
141
+ className="flex flex-col h-screen font-sans"
142
+ style={{ backgroundColor: colors.bg, color: colors.text }}
143
+ >
144
+ <Navbar
145
+ colors={colors}
146
+ theme={theme}
147
+ setTheme={setTheme}
148
+ data={filteredData}
149
+ />
150
+
151
+ <div className="flex flex-1 overflow-hidden">
152
+ <div ref={containerRef} className="flex-1 relative">
153
+ {filteredData && (
154
+ <GraphCanvas
155
+ data={filteredData}
156
+ dimensions={dimensions}
157
+ colors={colors}
158
+ effectiveTheme={effectiveTheme}
159
+ onNodeClick={setSelectedNode}
160
+ />
161
+ )}
162
+ </div>
163
+
164
+ {/* Right panel: Legend OR NodeDetails */}
165
+ <div
166
+ className="w-80 border-l flex flex-col h-full"
167
+ style={{ backgroundColor: colors.panel, borderColor: colors.panelBorder }}
168
+ >
169
+ {selectedNode ? (
170
+ <NodeDetails
171
+ colors={colors}
172
+ selectedNode={selectedNode}
173
+ onClose={() => setSelectedNode(null)}
174
+ />
175
+ ) : (
176
+ <div className="flex-1 overflow-y-auto">
177
+ <LegendPanel
178
+ colors={colors}
179
+ visibleSeverities={visibleSeverities}
180
+ visibleEdgeTypes={visibleEdgeTypes}
181
+ onToggleSeverity={handleToggleSeverity}
182
+ onToggleEdgeType={handleToggleEdgeType}
183
+ />
184
+ </div>
185
+ )}
186
+ </div>
187
+ </div>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ export default App;
@@ -0,0 +1,39 @@
1
+ import { ThemeColors } from '../types';
2
+
3
+ interface ErrorDisplayProps {
4
+ colors: ThemeColors;
5
+ error: string;
6
+ }
7
+
8
+ export function ErrorDisplay({ colors, error }: ErrorDisplayProps) {
9
+ return (
10
+ <div className="flex h-screen items-center justify-center" style={{ backgroundColor: colors.bg, color: colors.text }}>
11
+ <div className="text-center max-w-md p-6">
12
+ <svg
13
+ className="w-16 h-16 mx-auto text-amber-500 mb-4"
14
+ fill="none"
15
+ viewBox="0 0 24 24"
16
+ stroke="currentColor"
17
+ >
18
+ <path
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ strokeWidth={2}
22
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
23
+ />
24
+ </svg>
25
+ <h2 className="text-xl font-semibold mb-2 text-amber-400">No Scan Data Found</h2>
26
+ <p className="mb-4" style={{ color: colors.textMuted }}>{error}</p>
27
+ <div
28
+ className="text-left p-4 rounded-lg text-sm font-mono"
29
+ style={{ backgroundColor: colors.cardBg }}
30
+ >
31
+ <p className="text-cyan-400"># Step 1: Run aiready scan</p>
32
+ <p className="mb-2" style={{ color: colors.textMuted }}>aiready scan . --output json</p>
33
+ <p className="text-cyan-400"># Step 2: Copy latest report to visualizer</p>
34
+ <p style={{ color: colors.textMuted }}>cp .aiready/aiready-report-*.json packages/visualizer/web/public/report-data.json</p>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,205 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import * as d3 from 'd3';
3
+ import { FileNode, GraphData, ThemeColors, EffectiveTheme } from '../types';
4
+ import { severityColors, edgeColors, GRAPH_CONFIG } from '../constants';
5
+ import { getEdgeDistance, getEdgeStrength, getEdgeOpacity, getEdgeStrokeWidth } from '../utils';
6
+
7
+ interface GraphCanvasProps {
8
+ data: GraphData;
9
+ dimensions: { width: number; height: number };
10
+ colors: ThemeColors;
11
+ effectiveTheme: EffectiveTheme;
12
+ onNodeClick: (node: FileNode | null) => void;
13
+ }
14
+
15
+ export function GraphCanvas({
16
+ data,
17
+ dimensions,
18
+ colors,
19
+ effectiveTheme,
20
+ onNodeClick,
21
+ }: GraphCanvasProps) {
22
+ const svgRef = useRef<SVGSVGElement>(null);
23
+ const zoomTransformRef = useRef<d3.ZoomTransform>(d3.zoomIdentity);
24
+
25
+ useEffect(() => {
26
+ if (!data || !svgRef.current || !data.nodes.length) return;
27
+
28
+ const svg = d3.select(svgRef.current);
29
+
30
+ // Get actual SVG dimensions from the DOM element
31
+ const svgRect = svgRef.current.getBoundingClientRect();
32
+ const width = svgRect.width;
33
+ const height = svgRect.height;
34
+
35
+ svg.selectAll('*').remove();
36
+
37
+ const g = svg.append('g');
38
+
39
+ // Setup zoom
40
+ const zoom = d3.zoom<SVGSVGElement, unknown>()
41
+ .scaleExtent([GRAPH_CONFIG.zoomMin, GRAPH_CONFIG.zoomMax])
42
+ .on('zoom', (event) => {
43
+ g.attr('transform', event.transform);
44
+ zoomTransformRef.current = event.transform;
45
+ });
46
+ svg.call(zoom);
47
+ svg.call(zoom.transform, zoomTransformRef.current);
48
+
49
+ // Prepare nodes and links
50
+ const nodes = data.nodes.map((d, i) => {
51
+ // Initialize nodes in a circle around center with slight random spread
52
+ const angle = (i / data.nodes.length) * Math.PI * 2;
53
+ const radius = 50 + Math.random() * 30;
54
+ return {
55
+ ...d,
56
+ x: width / 2 + Math.cos(angle) * radius,
57
+ y: height / 2 + Math.sin(angle) * radius,
58
+ };
59
+ });
60
+ const links = data.edges.map(d => ({ ...d }));
61
+
62
+ // Create force simulation
63
+ const simulation = d3.forceSimulation(nodes as d3.SimulationNodeDatum[])
64
+ .force(
65
+ 'link',
66
+ d3.forceLink(links)
67
+ .id((d: unknown) => (d as { id: string }).id)
68
+ .distance((d: unknown) => getEdgeDistance((d as { type: string }).type))
69
+ .strength((d: unknown) => getEdgeStrength((d as { type: string }).type))
70
+ )
71
+ .force('charge', d3.forceManyBody().strength(GRAPH_CONFIG.simulation.chargeStrength))
72
+ .force('center', d3.forceCenter(width / 2, height / 2))
73
+ .force('collision', d3.forceCollide().radius(GRAPH_CONFIG.collisionRadius))
74
+ .force('x', d3.forceX(width / 2).strength(GRAPH_CONFIG.simulation.centerStrength))
75
+ .force('y', d3.forceY(height / 2).strength(GRAPH_CONFIG.simulation.centerStrength));
76
+
77
+ // Create link group
78
+ const linkGroup = g.append('g').attr('class', 'links');
79
+ const link = linkGroup
80
+ .selectAll('line')
81
+ .data(links)
82
+ .enter()
83
+ .append('line')
84
+ .attr('stroke', (d: unknown) => edgeColors[(d as { type: string }).type] || edgeColors.default)
85
+ .attr('stroke-opacity', (d: unknown) => getEdgeOpacity((d as { type: string }).type))
86
+ .attr('stroke-width', (d: unknown) => getEdgeStrokeWidth((d as { type: string }).type));
87
+
88
+ // Create node group
89
+ const nodeGroup = g.append('g').attr('class', 'nodes');
90
+
91
+ const node = nodeGroup
92
+ .selectAll('g')
93
+ .data(nodes)
94
+ .enter()
95
+ .append('g')
96
+ .attr('cursor', 'pointer')
97
+ .call(
98
+ d3.drag<SVGGElement, any>()
99
+ .on('start', dragstarted)
100
+ .on('drag', dragged)
101
+ .on('end', dragended) as any
102
+ );
103
+
104
+ // Add circles to nodes
105
+ node
106
+ .append('circle')
107
+ .attr('r', (d: unknown) => Math.sqrt((d as { value: number }).value || 10) + GRAPH_CONFIG.nodeBaseRadius)
108
+ .attr('fill', (d: unknown) => (d as { color: string }).color || severityColors.default)
109
+ .attr('stroke', effectiveTheme === 'dark' ? '#fff' : '#000')
110
+ .attr('stroke-width', 1.5);
111
+
112
+ // Add labels to nodes
113
+ node
114
+ .append('text')
115
+ .text((d: unknown) => ((d as { label: string }).label.split('/').pop() || (d as { label: string }).label))
116
+ .attr('x', 0)
117
+ .attr('y', (d: unknown) => Math.sqrt((d as { value: number }).value || 10) + 12)
118
+ .attr('text-anchor', 'middle')
119
+ .attr('fill', effectiveTheme === 'dark' ? '#e2e8f0' : '#1e293b')
120
+ .attr('font-size', '9px')
121
+ .attr('font-family', 'system-ui, sans-serif')
122
+ .attr('pointer-events', 'none');
123
+
124
+ // Add tooltips
125
+ node.append('title').text((d: unknown) => (d as { title: string }).title);
126
+
127
+ // Event handlers
128
+ node.on('click', (event: unknown, d: unknown) => {
129
+ (event as Event).stopPropagation();
130
+ onNodeClick(d as FileNode);
131
+ });
132
+ svg.on('click', () => onNodeClick(null));
133
+
134
+ // Simulation tick
135
+ simulation.on('tick', () => {
136
+ link
137
+ .attr('x1', (d: unknown) => (d as { source: { x: number } }).source.x)
138
+ .attr('y1', (d: unknown) => (d as { source: { y: number } }).source.y)
139
+ .attr('x2', (d: unknown) => (d as { target: { x: number } }).target.x)
140
+ .attr('y2', (d: unknown) => (d as { target: { y: number } }).target.y);
141
+
142
+ node.attr('transform', (d: unknown) => `translate(${(d as { x: number }).x},${(d as { y: number }).y})`);
143
+ });
144
+
145
+ // Drag functions
146
+ function dragstarted(event: unknown, d: unknown) {
147
+ if (!(event as { active: boolean }).active) simulation.alphaTarget(0.3).restart();
148
+ (d as { fx: number | null }).fx = (d as { x: number }).x;
149
+ (d as { fy: number | null }).fy = (d as { y: number }).y;
150
+ }
151
+
152
+ function dragged(event: unknown, d: unknown) {
153
+ (d as { fx: number }).fx = (event as { x: number }).x;
154
+ (d as { fy: number }).fy = (event as { y: number }).y;
155
+ }
156
+
157
+ function dragended(event: unknown, d: unknown) {
158
+ if (!(event as { active: boolean }).active) simulation.alphaTarget(0);
159
+ (d as { fx: null }).fx = null;
160
+ (d as { fy: null }).fy = null;
161
+ }
162
+
163
+ return () => {
164
+ simulation.stop();
165
+ };
166
+ }, [data, dimensions, effectiveTheme, onNodeClick]);
167
+
168
+ return (
169
+ <div className="flex-1 relative w-full h-full">
170
+ <div
171
+ className="absolute inset-0 pointer-events-none z-0"
172
+ style={{
173
+ backgroundImage: `linear-gradient(${colors.grid} 1px, transparent 1px), linear-gradient(90deg, ${colors.grid} 1px, transparent 1px)`,
174
+ backgroundSize: '40px 40px',
175
+ }}
176
+ />
177
+ <svg
178
+ ref={svgRef}
179
+ width="100%"
180
+ height="100%"
181
+ style={{ display: 'block', backgroundColor: 'transparent', zIndex: 10, position: 'absolute', top: 0, left: 0 }}
182
+ />
183
+ <div className="absolute bottom-6 left-6 z-20">
184
+ <div
185
+ className="px-4 py-2.5 rounded-lg backdrop-blur-sm border text-xs flex items-center gap-2"
186
+ style={{
187
+ backgroundColor: `${colors.panel}ee`,
188
+ borderColor: colors.panelBorder,
189
+ color: colors.textMuted,
190
+ }}
191
+ >
192
+ <svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
193
+ <path
194
+ strokeLinecap="round"
195
+ strokeLinejoin="round"
196
+ strokeWidth={2}
197
+ d="M7 11.5V14m0-2.5v-6a1.5 1.5 0 113 0m-3 6a1.5 1.5 0 00-3 0v2a7.5 7.5 0 0015 0v-5a1.5 1.5 0 00-3 0m-6-3V11m0-5.5v-1a1.5 1.5 0 013 0v1m0 0V11m0-5.5a1.5 1.5 0 013 0v3m0 0V11"
198
+ />
199
+ </svg>
200
+ <span>Drag to move • Scroll to zoom • Click for details</span>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ );
205
+ }