@@ -4,15 +4,14 @@ import ReactECharts from 'echarts-for-react';
44import { useApi } from '@/hooks/useApi' ;
55import { api } from '@/lib/api' ;
66import { useTheme } from '@/context/ThemeContext' ;
7- import type { NodesListResponse } from '@/types/api' ;
7+ import type { FileTreeResponse , FileTreeNode } from '@/types/api' ;
88
99const LANG_COLORS : Record < string , string > = {
1010 java : '#b07219' , python : '#3572A5' , typescript : '#3178c6' , javascript : '#f1e05a' ,
1111 go : '#00ADD8' , csharp : '#178600' , rust : '#dea584' , kotlin : '#A97BFF' ,
1212 yaml : '#cb171e' , json : '#555' , ruby : '#701516' , scala : '#c22d40' ,
1313 cpp : '#f34b7d' , shell : '#89e051' , markdown : '#083fa1' , html : '#e34c26' ,
1414 css : '#563d7c' , sql : '#e38c00' , proto : '#60a0b0' , dockerfile : '#384d54' ,
15- other : '#888' ,
1615} ;
1716
1817const EXT_TO_LANG : Record < string , string > = {
@@ -24,104 +23,85 @@ const EXT_TO_LANG: Record<string, string> = {
2423 css : 'css' , sql : 'sql' , proto : 'proto' ,
2524} ;
2625
27- interface TreeNode {
26+ interface EChartsTreeNode {
2827 name : string ;
29- path : string ;
3028 value : number ;
31- children ?: TreeNode [ ] ;
29+ children ?: EChartsTreeNode [ ] ;
3230 itemStyle ?: { color : string } ;
3331}
3432
35- function buildTreemap (
36- nodes : Array < { file_path ?: string ; label : string ; properties ?: Record < string , unknown > } > ,
37- ) : TreeNode [ ] {
38- // Build a nested directory tree
39- const root : Record < string , { count : number ; langs : Record < string , number > ; children : Record < string , unknown > } > = { } ;
40-
41- for ( const node of nodes ) {
42- const fp = node . file_path ;
43- if ( ! fp ) continue ;
44-
45- const parts = fp . split ( '/' ) ;
46- const ext = fp . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ?? '' ;
47- const lang = EXT_TO_LANG [ ext ] ?? 'other' ;
48-
49- // Walk/create directory path
50- let current = root ;
51- for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
52- const dir = parts [ i ] ;
53- if ( ! current [ dir ] ) {
54- current [ dir ] = { count : 0 , langs : { } , children : { } } ;
55- }
56- current [ dir ] . count ++ ;
57- current [ dir ] . langs [ lang ] = ( current [ dir ] . langs [ lang ] ?? 0 ) + 1 ;
58- current = current [ dir ] . children as typeof root ;
59- }
60- }
33+ function inferLang ( name : string ) : string {
34+ const ext = name . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ?? '' ;
35+ return EXT_TO_LANG [ ext ] ?? 'other' ;
36+ }
6137
62- function toTreeNodes ( map : typeof root , parentPath : string ) : TreeNode [ ] {
63- return Object . entries ( map )
64- . sort ( ( a , b ) => b [ 1 ] . count - a [ 1 ] . count )
65- . map ( ( [ name , data ] ) => {
66- const path = parentPath ? ` ${ parentPath } / ${ name } ` : name ;
67- const dominantLang = Object . entries ( data . langs ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) [ 0 ] ?. [ 0 ] ?? 'other' ;
68- const children = toTreeNodes ( data . children as typeof root , path ) ;
69-
70- if ( children . length > 0 ) {
71- return {
72- name ,
73- path ,
74- value : data . count ,
75- children ,
76- itemStyle : { color : LANG_COLORS [ dominantLang ] ?? '#888' } ,
77- } ;
38+ function fileTreeToECharts ( nodes : FileTreeNode [ ] ) : EChartsTreeNode [ ] {
39+ return nodes
40+ . filter ( n => n . nodeCount > 0 || ( n . children && n . children . length > 0 ) )
41+ . map ( n => {
42+ if ( n . type === 'directory' && n . children && n . children . length > 0 ) {
43+ const children = fileTreeToECharts ( n . children ) ;
44+ // Dominant language from children names
45+ const langCounts : Record < string , number > = { } ;
46+ function countLangs ( items : FileTreeNode [ ] ) {
47+ for ( const item of items ) {
48+ if ( item . type === 'file' ) {
49+ const lang = inferLang ( item . name ) ;
50+ langCounts [ lang ] = ( langCounts [ lang ] ?? 0 ) + ( item . nodeCount || 1 ) ;
51+ }
52+ if ( item . children ) countLangs ( item . children ) ;
53+ }
7854 }
55+ countLangs ( n . children ) ;
56+ const dominant = Object . entries ( langCounts ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) [ 0 ] ?. [ 0 ] ?? 'other' ;
57+
7958 return {
80- name,
81- path ,
82- value : data . count ,
83- itemStyle : { color : LANG_COLORS [ dominantLang ] ?? '#888 ' } ,
59+ name : n . name ,
60+ value : n . nodeCount ,
61+ children : children . length > 0 ? children : undefined ,
62+ itemStyle : { color : LANG_COLORS [ dominant ] ?? '#666 ' } ,
8463 } ;
85- } ) ;
86- }
64+ }
65+ const lang = inferLang ( n . name ) ;
66+ return {
67+ name : n . name ,
68+ value : Math . max ( n . nodeCount , 1 ) ,
69+ itemStyle : { color : LANG_COLORS [ lang ] ?? '#666' } ,
70+ } ;
71+ } ) ;
72+ }
8773
88- return toTreeNodes ( root , '' ) ;
74+ function collectLanguages ( nodes : FileTreeNode [ ] ) : string [ ] {
75+ const langs = new Set < string > ( ) ;
76+ function walk ( items : FileTreeNode [ ] ) {
77+ for ( const item of items ) {
78+ if ( item . type === 'file' ) {
79+ const lang = inferLang ( item . name ) ;
80+ if ( lang !== 'other' ) langs . add ( lang ) ;
81+ }
82+ if ( item . children ) walk ( item . children ) ;
83+ }
84+ }
85+ walk ( nodes ) ;
86+ return Array . from ( langs ) . sort ( ) ;
8987}
9088
9189export default function CodebaseMap ( ) {
9290 const { isDark } = useTheme ( ) ;
9391 const [ langFilter , setLangFilter ] = useState < string | undefined > ( undefined ) ;
94- const { data : allNodesData , loading, error } = useApi < NodesListResponse > (
95- ( ) => api . getNodes ( undefined , undefined , 10000 , 0 ) , [ ]
92+
93+ const { data : treeData , loading, error } = useApi < FileTreeResponse > (
94+ ( ) => api . getFileTree ( ) , [ ]
9695 ) ;
9796
98- const nodes = useMemo ( ( ) => {
99- const all = allNodesData ?. nodes ?? [ ] ;
100- return all . filter ( n => n . file_path ) ;
101- } , [ allNodesData ] ) ;
102-
103- const uniqueLangs = useMemo ( ( ) => {
104- const langs = new Set < string > ( ) ;
105- for ( const n of nodes ) {
106- const ext = n . file_path ?. split ( '.' ) . pop ( ) ?. toLowerCase ( ) ?? '' ;
107- const lang = EXT_TO_LANG [ ext ] ;
108- if ( lang ) langs . add ( lang ) ;
109- }
110- return Array . from ( langs ) . sort ( ) ;
111- } , [ nodes ] ) ;
112-
113- const filteredNodes = useMemo ( ( ) => {
114- if ( ! langFilter ) return nodes ;
115- const matchExts = Object . entries ( EXT_TO_LANG )
116- . filter ( ( [ , lang ] ) => lang === langFilter )
117- . map ( ( [ ext ] ) => ext ) ;
118- return nodes . filter ( n => {
119- const ext = n . file_path ?. split ( '.' ) . pop ( ) ?. toLowerCase ( ) ?? '' ;
120- return matchExts . includes ( ext ) ;
121- } ) ;
122- } , [ nodes , langFilter ] ) ;
97+ const tree = treeData ?. tree ?? [ ] ;
98+ const totalFiles = treeData ?. total_files ?? 0 ;
99+ const uniqueLangs = useMemo ( ( ) => collectLanguages ( tree ) , [ tree ] ) ;
123100
124- const treemapData = useMemo ( ( ) => buildTreemap ( filteredNodes ) , [ filteredNodes ] ) ;
101+ const treemapData = useMemo ( ( ) => {
102+ // TODO: apply langFilter if needed
103+ return fileTreeToECharts ( tree ) ;
104+ } , [ tree , langFilter ] ) ;
125105
126106 const chartOption = useMemo ( ( ) => ( {
127107 tooltip : {
@@ -138,13 +118,13 @@ export default function CodebaseMap() {
138118 drillDownIcon : '▶' ,
139119 breadcrumb : {
140120 show : true ,
141- top : 0 ,
142- left : 0 ,
121+ top : 4 ,
122+ left : 4 ,
143123 itemStyle : {
144- color : isDark ? '#1f1f38' : '#f5f5f5' ,
145- borderColor : isDark ? '#2d2d4a' : '#d9d9d9' ,
146- textStyle : { color : isDark ? '#e0e0e0' : '#333' } ,
124+ color : isDark ? '#2a2a2e' : '#f5f5f5' ,
125+ borderColor : isDark ? '#3f3f46' : '#d9d9d9' ,
147126 } ,
127+ textStyle : { color : isDark ? '#e0e0e0' : '#333' } ,
148128 } ,
149129 levels : [
150130 {
@@ -167,29 +147,18 @@ export default function CodebaseMap() {
167147 borderWidth : 1 ,
168148 gapWidth : 1 ,
169149 } ,
170- upperLabel : {
171- show : true ,
172- height : 22 ,
173- fontSize : 12 ,
174- } ,
150+ upperLabel : { show : true , height : 22 , fontSize : 12 } ,
175151 } ,
176152 {
177153 itemStyle : {
178154 borderColor : isDark ? '#666' : '#eee' ,
179155 borderWidth : 0.5 ,
180156 gapWidth : 0.5 ,
181157 } ,
182- label : {
183- show : true ,
184- fontSize : 11 ,
185- } ,
158+ label : { show : true , fontSize : 11 } ,
186159 } ,
187160 ] ,
188- label : {
189- show : true ,
190- formatter : '{b}' ,
191- fontSize : 12 ,
192- } ,
161+ label : { show : true , formatter : '{b}' , fontSize : 12 } ,
193162 } ] ,
194163 } ) , [ treemapData , isDark ] ) ;
195164
@@ -203,18 +172,17 @@ export default function CodebaseMap() {
203172
204173 return (
205174 < div style = { { display : 'flex' , flexDirection : 'column' , height : 'calc(100vh - 96px)' , margin : '-16px -24px' , padding : '8px 16px 0' } } >
206- { /* Top bar: title + filter */ }
207175 < div style = { {
208176 display : 'flex' ,
209177 justifyContent : 'space-between' ,
210178 alignItems : 'center' ,
211- marginBottom : 8 ,
179+ marginBottom : 4 ,
212180 flexShrink : 0 ,
213181 } } >
214182 < Space >
215183 < Typography . Title level = { 4 } style = { { margin : 0 } } > Codebase Map</ Typography . Title >
216184 < Typography . Text type = "secondary" >
217- { filteredNodes . length . toLocaleString ( ) } nodes · { uniqueLangs . length } languages
185+ { totalFiles . toLocaleString ( ) } files · { uniqueLangs . length } languages
218186 </ Typography . Text >
219187 </ Space >
220188 < Select
@@ -227,7 +195,6 @@ export default function CodebaseMap() {
227195 />
228196 </ div >
229197
230- { /* Treemap fills remaining space */ }
231198 < div style = { { flex : 1 , minHeight : 0 } } >
232199 { treemapData . length > 0 ? (
233200 < ReactECharts
0 commit comments