1- import { useState , useMemo } from 'react' ;
2- import { Select , Typography , Space , Spin , Alert } from 'antd' ;
1+ import { useState , useMemo , useCallback } from 'react' ;
2+ import { Typography , Spin , Alert , Drawer } from 'antd' ;
33import ReactECharts from 'echarts-for-react' ;
44import { useApi } from '@/hooks/useApi' ;
55import { api } from '@/lib/api' ;
@@ -51,16 +51,10 @@ function dominantLang(nodes: FileTreeNode[]): string {
5151 return Object . entries ( counts ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) [ 0 ] ?. [ 0 ] ?? 'other' ;
5252}
5353
54- /**
55- * Collapse single-child directory chains into one node.
56- * e.g., src → main → java → io → github becomes "src/main/java/io/github"
57- * This avoids 10+ clicks through single-child directories (common in Java packages).
58- */
5954function collapseTree ( nodes : FileTreeNode [ ] ) : FileTreeNode [ ] {
6055 return nodes . map ( n => {
6156 if ( n . type !== 'directory' || ! n . children || n . children . length === 0 ) return n ;
6257
63- // Collapse: if this directory has exactly 1 child that is also a directory, merge names
6458 let current = n ;
6559 let collapsedName = n . name ;
6660 while (
@@ -89,14 +83,12 @@ function toEChartsNodes(nodes: FileTreeNode[]): EChartsTreeNode[] {
8983 const children = toEChartsNodes ( n . children ) ;
9084 if ( children . length === 0 ) continue ;
9185 const lang = dominantLang ( n . children ) ;
92- // Directory nodes: NO value — ECharts sums from children for correct proportions
9386 result . push ( {
9487 name : n . name ,
9588 children,
9689 itemStyle : { color : LANG_COLORS [ lang ] ?? '#666' } ,
9790 } ) ;
9891 } else {
99- // Leaf file node: value = nodeCount (determines rectangle size)
10092 const lang = inferLang ( n . name ) ;
10193 result . push ( {
10294 name : n . name ,
@@ -112,34 +104,43 @@ function fileTreeToECharts(nodes: FileTreeNode[]): EChartsTreeNode[] {
112104 return toEChartsNodes ( collapseTree ( nodes ) ) ;
113105}
114106
115- function collectLanguages ( nodes : FileTreeNode [ ] ) : string [ ] {
116- const langs = new Set < string > ( ) ;
117- function walk ( items : FileTreeNode [ ] ) {
118- for ( const item of items ) {
119- if ( item . type === 'file' ) {
120- const lang = inferLang ( item . name ) ;
121- if ( lang !== 'other' ) langs . add ( lang ) ;
122- }
123- if ( item . children ) walk ( item . children ) ;
124- }
125- }
126- walk ( nodes ) ;
127- return Array . from ( langs ) . sort ( ) ;
128- }
129-
130107export default function CodebaseMap ( ) {
131108 const { isDark } = useTheme ( ) ;
132- const [ langFilter , setLangFilter ] = useState < string | undefined > ( undefined ) ;
109+ const [ fileDrawer , setFileDrawer ] = useState < { path : string ; content : string } | null > ( null ) ;
110+ const [ fileLoading , setFileLoading ] = useState ( false ) ;
133111
134112 const { data : treeData , loading, error } = useApi < FileTreeResponse > (
135113 ( ) => api . getFileTree ( ) , [ ]
136114 ) ;
137115
138116 const tree = treeData ?. tree ?? [ ] ;
139117 const totalFiles = treeData ?. total_files ?? 0 ;
140- const uniqueLangs = useMemo ( ( ) => collectLanguages ( tree ) , [ tree ] ) ;
141118 const treemapData = useMemo ( ( ) => fileTreeToECharts ( tree ) , [ tree ] ) ;
142119
120+ // On click: if leaf node (no children), open file in drawer
121+ const onClickNode = useCallback ( async ( params : {
122+ data ?: { children ?: unknown [ ] } ;
123+ treePathInfo ?: Array < { name : string } > ;
124+ } ) => {
125+ if ( params . data ?. children && ( params . data . children as unknown [ ] ) . length > 0 ) return ;
126+ const pathParts = params . treePathInfo ?. map ( p => p . name ) . filter ( Boolean ) ?? [ ] ;
127+ if ( pathParts . length === 0 ) return ;
128+ const filePath = pathParts . join ( '/' ) ;
129+ setFileLoading ( true ) ;
130+ try {
131+ const content = await api . readFile ( filePath ) ;
132+ setFileDrawer ( { path : filePath , content } ) ;
133+ } catch {
134+ setFileDrawer ( { path : filePath , content : '// Could not load file' } ) ;
135+ } finally {
136+ setFileLoading ( false ) ;
137+ }
138+ } , [ ] ) ;
139+
140+ const onEvents = useMemo ( ( ) => ( {
141+ click : onClickNode ,
142+ } ) , [ onClickNode ] ) ;
143+
143144 const chartOption = useMemo ( ( ) => ( {
144145 tooltip : {
145146 formatter : ( info : { name : string ; value : number ; treePathInfo ?: Array < { name : string } > } ) => {
@@ -150,21 +151,32 @@ export default function CodebaseMap() {
150151 series : [ {
151152 type : 'treemap' ,
152153 data : treemapData ,
154+ top : 0 ,
155+ left : 0 ,
156+ right : 0 ,
157+ bottom : 0 ,
158+ width : '100%' ,
159+ height : '100%' ,
153160 leafDepth : 2 ,
154161 drillDownIcon : '▶ ' ,
155162 roam : false ,
156163 nodeClick : 'zoomToNode' ,
157164 breadcrumb : {
158165 show : true ,
159- top : 4 ,
160- left : 4 ,
166+ bottom : 8 ,
167+ left : 'center' ,
168+ height : 28 ,
161169 itemStyle : {
162- color : isDark ? '#1a1a1a' : '#f5f5f5' ,
163- borderColor : isDark ? '#303030' : '#d9d9d9' ,
170+ color : isDark ? '#1f1f1f' : '#fff' ,
171+ borderColor : isDark ? '#444' : '#bbb' ,
172+ borderWidth : 1 ,
173+ shadowBlur : 3 ,
174+ shadowColor : isDark ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.15)' ,
164175 } ,
165176 textStyle : {
166177 color : isDark ? '#e0e0e0' : '#333' ,
167- fontSize : 13 ,
178+ fontSize : 14 ,
179+ fontWeight : 'bold' as const ,
168180 } ,
169181 } ,
170182 levels : [
@@ -222,44 +234,61 @@ export default function CodebaseMap() {
222234 }
223235
224236 return (
225- < div style = { { display : 'flex' , flexDirection : 'column' , height : 'calc(100vh - 96px)' , margin : '-16px -24px' , padding : '8px 16px 0' } } >
226- < div style = { {
227- display : 'flex' ,
228- justifyContent : 'space-between' ,
229- alignItems : 'center' ,
230- marginBottom : 4 ,
231- flexShrink : 0 ,
232- } } >
233- < Space >
234- < Typography . Title level = { 4 } style = { { margin : 0 } } > Codebase Map</ Typography . Title >
235- < Typography . Text type = "secondary" >
236- { totalFiles . toLocaleString ( ) } files · { uniqueLangs . length } languages
237- </ Typography . Text >
238- </ Space >
239- < Select
240- allowClear
241- placeholder = "Filter by language"
242- style = { { width : 180 } }
243- value = { langFilter }
244- onChange = { setLangFilter }
245- options = { uniqueLangs . map ( l => ( { label : l . charAt ( 0 ) . toUpperCase ( ) + l . slice ( 1 ) , value : l } ) ) }
246- />
247- </ div >
248-
249- < div style = { { flex : 1 , minHeight : 0 } } >
250- { treemapData . length > 0 ? (
237+ < div style = { { position : 'relative' , height : 'calc(100vh - 64px)' , margin : '-16px -24px' } } >
238+ { treemapData . length > 0 ? (
239+ < >
240+ < div style = { {
241+ position : 'absolute' ,
242+ top : 6 ,
243+ right : 10 ,
244+ zIndex : 10 ,
245+ background : isDark ? 'rgba(10,10,10,0.8)' : 'rgba(255,255,255,0.85)' ,
246+ borderRadius : 4 ,
247+ padding : '2px 10px' ,
248+ fontSize : 12 ,
249+ color : isDark ? '#888' : '#999' ,
250+ } } >
251+ { totalFiles . toLocaleString ( ) } files
252+ </ div >
251253 < ReactECharts
252254 option = { chartOption }
253255 style = { { height : '100%' , width : '100%' } }
254256 theme = { isDark ? 'dark' : undefined }
255257 opts = { { renderer : 'canvas' } }
258+ onEvents = { onEvents }
256259 />
260+ </ >
261+ ) : (
262+ < div style = { { textAlign : 'center' , padding : 60 } } >
263+ < Typography . Text type = "secondary" > No file data available. Run index + enrich first.</ Typography . Text >
264+ </ div >
265+ ) }
266+
267+ < Drawer
268+ title = { fileDrawer ?. path }
269+ placement = "right"
270+ width = "60%"
271+ open = { ! ! fileDrawer }
272+ onClose = { ( ) => setFileDrawer ( null ) }
273+ styles = { { body : { padding : 0 } } }
274+ >
275+ { fileLoading ? (
276+ < div style = { { textAlign : 'center' , padding : 40 } } > < Spin /> </ div >
257277 ) : (
258- < div style = { { textAlign : 'center' , padding : 60 } } >
259- < Typography . Text type = "secondary" > No file data available. Run index + enrich first.</ Typography . Text >
260- </ div >
278+ < pre style = { {
279+ margin : 0 ,
280+ padding : 16 ,
281+ fontSize : 13 ,
282+ lineHeight : 1.5 ,
283+ overflow : 'auto' ,
284+ height : '100%' ,
285+ background : isDark ? '#0a0a0a' : '#fafafa' ,
286+ color : isDark ? '#d4d4d4' : '#1f1f1f' ,
287+ } } >
288+ { fileDrawer ?. content }
289+ </ pre >
261290 ) }
262- </ div >
291+ </ Drawer >
263292 </ div >
264293 ) ;
265294}
0 commit comments