@@ -14,6 +14,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
14
14
import { Dialog , DialogContent , DialogHeader , DialogTitle , DialogTrigger } from "@/components/ui/dialog"
15
15
import { Input } from "@/components/ui/input"
16
16
import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@/components/ui/select"
17
+ import { Alert , AlertDescription } from "@/components/ui/alert"
17
18
import {
18
19
analyzeRepository ,
19
20
buildStructureString ,
@@ -22,18 +23,22 @@ import {
22
23
generateStructure ,
23
24
validateGitHubUrl ,
24
25
validateGitLabUrl ,
26
+ type RepoValidationResult ,
27
+ PERFORMANCE_THRESHOLDS ,
25
28
} from "@/lib/repo-tree-utils"
26
29
import { convertMapToJson } from "@/lib/utils"
27
30
import type { TreeCustomizationOptions } from "@/types/tree-customization"
28
31
import { saveAs } from "file-saver"
29
32
import {
33
+ AlertTriangle ,
30
34
Check ,
31
35
ChevronDown ,
32
36
CircleX ,
33
37
Copy ,
34
38
Download ,
35
39
Github ,
36
40
GitlabIcon as GitLab ,
41
+ Info ,
37
42
Maximize ,
38
43
Minimize ,
39
44
RefreshCw ,
@@ -110,6 +115,9 @@ export default function RepoProjectStructure() {
110
115
message : "" ,
111
116
isError : false ,
112
117
} )
118
+ const [ repoValidation , setRepoValidation ] = useState < RepoValidationResult | null > ( null )
119
+ const [ showValidationDialog , setShowValidationDialog ] = useState ( false )
120
+ const [ proceedWithLargeRepo , setProceedWithLargeRepo ] = useState ( false )
113
121
const [ copied , setCopied ] = useState ( false )
114
122
const [ expanded , setExpanded ] = useState ( false )
115
123
const [ viewMode , setViewMode ] = useState < "ascii" | "interactive" > ( "ascii" )
@@ -145,7 +153,7 @@ export default function RepoProjectStructure() {
145
153
)
146
154
147
155
const handleFetchStructure = useCallback (
148
- async ( url : string = repoUrl ) => {
156
+ async ( url : string = repoUrl , skipValidation : boolean = false ) => {
149
157
if ( ! url ) {
150
158
setValidation ( { message : "Repository URL is required" , isError : true } )
151
159
return
@@ -161,7 +169,22 @@ export default function RepoProjectStructure() {
161
169
162
170
setLoading ( true )
163
171
try {
164
- const tree = await fetchProjectStructure ( url , repoType )
172
+ const { tree, validation : repoVal } = await fetchProjectStructure ( url , repoType )
173
+ setRepoValidation ( repoVal )
174
+
175
+ // Check if we should show validation warnings
176
+ if ( ! skipValidation && ! repoVal . isValid ) {
177
+ setShowValidationDialog ( true )
178
+ setLoading ( false )
179
+ return
180
+ }
181
+
182
+ if ( ! skipValidation && repoVal . warnings . length > 0 && ! proceedWithLargeRepo ) {
183
+ setShowValidationDialog ( true )
184
+ setLoading ( false )
185
+ return
186
+ }
187
+
165
188
const map = generateStructure ( tree )
166
189
setStructureMap ( map )
167
190
setValidation ( { message : "" , isError : false } )
@@ -170,6 +193,10 @@ export default function RepoProjectStructure() {
170
193
const { fileTypes, languages } = analyzeRepository ( map )
171
194
setFileTypeData ( fileTypes )
172
195
setLanguageData ( languages )
196
+
197
+ // Reset validation dialog state
198
+ setShowValidationDialog ( false )
199
+ setProceedWithLargeRepo ( false )
173
200
} catch ( err : unknown ) {
174
201
if ( err instanceof Error ) {
175
202
console . error ( err )
@@ -184,12 +211,19 @@ export default function RepoProjectStructure() {
184
211
isError : true ,
185
212
} )
186
213
}
214
+ setRepoValidation ( null )
187
215
}
188
216
setLoading ( false )
189
217
} ,
190
- [ repoUrl , repoType ] ,
218
+ [ repoUrl , repoType , proceedWithLargeRepo ] ,
191
219
)
192
220
221
+ const handleProceedWithLargeRepo = useCallback ( ( ) => {
222
+ setProceedWithLargeRepo ( true )
223
+ setShowValidationDialog ( false )
224
+ handleFetchStructure ( repoUrl , true )
225
+ } , [ repoUrl , handleFetchStructure ] )
226
+
193
227
useEffect ( ( ) => {
194
228
const savedUrl = localStorage . getItem ( "lastRepoUrl" )
195
229
if ( savedUrl ) {
@@ -219,17 +253,21 @@ export default function RepoProjectStructure() {
219
253
}
220
254
} , [ ] )
221
255
256
+ // Memoized filtering with performance optimization
222
257
const filterStructure = useCallback ( ( map : DirectoryMap , term : string ) : DirectoryMap => {
258
+ if ( ! term . trim ( ) ) return map
259
+
223
260
const filteredMap : DirectoryMap = new Map ( )
261
+ const lowerTerm = term . toLowerCase ( )
224
262
225
263
for ( const [ key , value ] of map . entries ( ) ) {
226
264
if ( value && typeof value === "object" && "type" in value && value . type === "file" ) {
227
- if ( key . toLowerCase ( ) . includes ( term . toLowerCase ( ) ) ) {
265
+ if ( key . toLowerCase ( ) . includes ( lowerTerm ) ) {
228
266
filteredMap . set ( key , value )
229
267
}
230
268
} else if ( value instanceof Map ) {
231
269
const filteredSubMap = filterStructure ( value , term )
232
- if ( filteredSubMap . size > 0 || key . toLowerCase ( ) . includes ( term . toLowerCase ( ) ) ) {
270
+ if ( filteredSubMap . size > 0 || key . toLowerCase ( ) . includes ( lowerTerm ) ) {
233
271
filteredMap . set ( key , filteredSubMap )
234
272
}
235
273
}
@@ -243,10 +281,15 @@ export default function RepoProjectStructure() {
243
281
[ filterStructure , structureMap , searchTerm ] ,
244
282
)
245
283
246
- const customizedStructure = useMemo (
247
- ( ) => buildStructureString ( filteredStructureMap , "" , customizationOptions ) ,
248
- [ filteredStructureMap , customizationOptions ] ,
249
- )
284
+ // Memoized structure string with performance optimization
285
+ const customizedStructure = useMemo ( ( ) => {
286
+ // For very large structures, limit rendering to prevent performance issues
287
+ const mapSize = structureMap . size
288
+ if ( mapSize > PERFORMANCE_THRESHOLDS . LARGE_REPO_ENTRIES ) {
289
+ return buildStructureString ( filteredStructureMap , "" , customizationOptions , "" , 20 ) // Limit depth
290
+ }
291
+ return buildStructureString ( filteredStructureMap , "" , customizationOptions )
292
+ } , [ filteredStructureMap , customizationOptions , structureMap . size ] )
250
293
251
294
const copyToClipboard = useCallback ( ( ) => {
252
295
navigator . clipboard . writeText ( customizedStructure ) . then ( ( ) => {
@@ -259,6 +302,7 @@ export default function RepoProjectStructure() {
259
302
setRepoUrl ( "" )
260
303
localStorage . removeItem ( "lastRepoUrl" )
261
304
setStructureMap ( new Map ( ) )
305
+ setRepoValidation ( null )
262
306
if ( inputRef . current ) {
263
307
inputRef . current . focus ( )
264
308
}
@@ -275,19 +319,19 @@ export default function RepoProjectStructure() {
275
319
276
320
switch ( format ) {
277
321
case "md" :
278
- content = `# Repository Structure\n\n\`\`\`\n${ customizedStructure } \`\`\``
322
+ content = `# Directory Structure\n\n\`\`\`\n${ customizedStructure } \`\`\``
279
323
mimeType = "text/markdown;charset=utf-8"
280
324
fileName = "README.md"
281
325
break
282
326
case "txt" :
283
327
content = customizedStructure
284
328
mimeType = "text/plain;charset=utf-8"
285
- fileName = "repository -structure.txt"
329
+ fileName = "directory -structure.txt"
286
330
break
287
331
case "json" :
288
332
content = JSON . stringify ( convertMapToJson ( filteredStructureMap ) , null , 2 )
289
333
mimeType = "application/json;charset=utf-8"
290
- fileName = "repository -structure.json"
334
+ fileName = "directory -structure.json"
291
335
break
292
336
case "html" :
293
337
content = `
@@ -305,7 +349,7 @@ export default function RepoProjectStructure() {
305
349
</html>
306
350
`
307
351
mimeType = "text/html;charset=utf-8"
308
- fileName = "repository -structure.html"
352
+ fileName = "directory -structure.html"
309
353
break
310
354
}
311
355
@@ -334,6 +378,79 @@ export default function RepoProjectStructure() {
334
378
335
379
return (
336
380
< div >
381
+ { /* Validation Dialog */ }
382
+ < Dialog open = { showValidationDialog } onOpenChange = { setShowValidationDialog } >
383
+ < DialogContent className = "sm:max-w-[500px]" >
384
+ < DialogHeader >
385
+ < DialogTitle className = "flex items-center gap-2" >
386
+ { repoValidation ?. isValid === false ? (
387
+ < AlertTriangle className = "h-5 w-5 text-red-500" />
388
+ ) : (
389
+ < Info className = "h-5 w-5 text-yellow-500" />
390
+ ) }
391
+ Repository Size Warning
392
+ </ DialogTitle >
393
+ </ DialogHeader >
394
+
395
+ { repoValidation && (
396
+ < div className = "space-y-4" >
397
+ < div className = "text-sm text-gray-600 dark:text-gray-300" >
398
+ < p className = "font-medium mb-2" > Repository Statistics:</ p >
399
+ < ul className = "space-y-1" >
400
+ < li > • Total entries: { repoValidation . totalEntries . toLocaleString ( ) } </ li >
401
+ < li > • Estimated size: { ( repoValidation . estimatedSize / ( 1024 * 1024 ) ) . toFixed ( 2 ) } MB</ li >
402
+ </ ul >
403
+ </ div >
404
+
405
+ { repoValidation . errors . length > 0 && (
406
+ < Alert className = "border-red-200 bg-red-50 dark:bg-red-950/20" >
407
+ < AlertTriangle className = "h-4 w-4 text-red-500" />
408
+ < AlertDescription className = "text-red-700 dark:text-red-300" >
409
+ < ul className = "space-y-1" >
410
+ { repoValidation . errors . map ( ( error , index ) => (
411
+ < li key = { index } > • { error } </ li >
412
+ ) ) }
413
+ </ ul >
414
+ </ AlertDescription >
415
+ </ Alert >
416
+ ) }
417
+
418
+ { repoValidation . warnings . length > 0 && (
419
+ < Alert className = "border-yellow-200 bg-yellow-50 dark:bg-yellow-950/20" >
420
+ < AlertTriangle className = "h-4 w-4 text-yellow-500" />
421
+ < AlertDescription className = "text-yellow-700 dark:text-yellow-300" >
422
+ < ul className = "space-y-1" >
423
+ { repoValidation . warnings . map ( ( warning , index ) => (
424
+ < li key = { index } > • { warning } </ li >
425
+ ) ) }
426
+ </ ul >
427
+ </ AlertDescription >
428
+ </ Alert >
429
+ ) }
430
+
431
+ < div className = "flex gap-3 pt-4" >
432
+ { repoValidation . isValid && (
433
+ < Button
434
+ onClick = { handleProceedWithLargeRepo }
435
+ variant = "default"
436
+ className = "flex-1"
437
+ >
438
+ Continue Anyway
439
+ </ Button >
440
+ ) }
441
+ < Button
442
+ onClick = { ( ) => setShowValidationDialog ( false ) }
443
+ variant = "outline"
444
+ className = "flex-1"
445
+ >
446
+ Cancel
447
+ </ Button >
448
+ </ div >
449
+ </ div >
450
+ ) }
451
+ </ DialogContent >
452
+ </ Dialog >
453
+
337
454
< Card
338
455
className = "w-full max-w-5xl mx-auto p-2 md:p-8 bg-gradient-to-br from-blue-50 to-white dark:from-gray-900 dark:to-gray-800 shadow-xl"
339
456
id = "generator"
@@ -351,6 +468,20 @@ export default function RepoProjectStructure() {
351
468
{ /* Token Status */ }
352
469
< TokenStatus />
353
470
471
+ { /* Repository Validation Status */ }
472
+ { repoValidation && structureMap . size > 0 && (
473
+ < Alert className = "border-blue-200 bg-blue-50 dark:bg-blue-950/20" >
474
+ < Info className = "h-4 w-4 text-blue-500" />
475
+ < AlertDescription className = "text-blue-700 dark:text-blue-300" >
476
+ Repository processed: { repoValidation . totalEntries . toLocaleString ( ) } entries,
477
+ estimated size: { ( repoValidation . estimatedSize / ( 1024 * 1024 ) ) . toFixed ( 2 ) } MB
478
+ { repoValidation . totalEntries > PERFORMANCE_THRESHOLDS . LARGE_REPO_ENTRIES &&
479
+ " (Large repository - some features may be slower)"
480
+ }
481
+ </ AlertDescription >
482
+ </ Alert >
483
+ ) }
484
+
354
485
< div className = "grid grid-cols-1 sm:grid-cols-12 gap-4 items-end" >
355
486
{ /* Repository Type Select */ }
356
487
< div className = "sm:col-span-3" >
@@ -582,26 +713,28 @@ export default function RepoProjectStructure() {
582
713
{ /* Code Block */ }
583
714
< div className = "relative border border-gray-300 dark:border-gray-600 border-t-0 rounded-b-lg overflow-hidden" ref = { treeRef } >
584
715
{ viewMode === "ascii" ? (
585
- < SyntaxHighlighter
586
- language = "plaintext"
587
- style = { atomDark }
588
- className = { `${ expanded ? "max-h-[none]" : "max-h-96" } overflow-y-auto min-h-[200px]` }
589
- showLineNumbers = { customizationOptions . showLineNumbers }
590
- wrapLines = { true }
591
- customStyle = { {
592
- margin : 0 ,
593
- borderRadius : 0 ,
594
- border : 'none'
595
- } }
596
- >
597
- { customizedStructure
598
- ? customizedStructure
599
- : searchTerm
600
- ? noResultsMessage ( searchTerm )
601
- : noStructureMessage }
602
- </ SyntaxHighlighter >
716
+ < div style = { { contain : "layout style paint" } } > { /* CSS containment for performance */ }
717
+ < SyntaxHighlighter
718
+ language = "plaintext"
719
+ style = { atomDark }
720
+ className = { `${ expanded ? "max-h-[none]" : "max-h-96" } overflow-y-auto min-h-[200px]` }
721
+ showLineNumbers = { customizationOptions . showLineNumbers }
722
+ wrapLines = { true }
723
+ customStyle = { {
724
+ margin : 0 ,
725
+ borderRadius : 0 ,
726
+ border : 'none'
727
+ } }
728
+ >
729
+ { customizedStructure
730
+ ? customizedStructure
731
+ : searchTerm
732
+ ? noResultsMessage ( searchTerm )
733
+ : noStructureMessage }
734
+ </ SyntaxHighlighter >
735
+ </ div >
603
736
) : filteredStructureMap . size > 0 ? (
604
- < div className = "bg-gray-900 min-h-[200px] p-4" >
737
+ < div className = "bg-gray-900 min-h-[200px] p-4" style = { { contain : "layout style paint" } } >
605
738
< InteractiveTreeView structure = { filteredStructureMap } customizationOptions = { customizationOptions } />
606
739
</ div >
607
740
) : (
0 commit comments