@@ -14,6 +14,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
1414import { Dialog , DialogContent , DialogHeader , DialogTitle , DialogTrigger } from "@/components/ui/dialog"
1515import { Input } from "@/components/ui/input"
1616import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@/components/ui/select"
17+ import { Alert , AlertDescription } from "@/components/ui/alert"
1718import {
1819 analyzeRepository ,
1920 buildStructureString ,
@@ -22,18 +23,22 @@ import {
2223 generateStructure ,
2324 validateGitHubUrl ,
2425 validateGitLabUrl ,
26+ type RepoValidationResult ,
27+ PERFORMANCE_THRESHOLDS ,
2528} from "@/lib/repo-tree-utils"
2629import { convertMapToJson } from "@/lib/utils"
2730import type { TreeCustomizationOptions } from "@/types/tree-customization"
2831import { saveAs } from "file-saver"
2932import {
33+ AlertTriangle ,
3034 Check ,
3135 ChevronDown ,
3236 CircleX ,
3337 Copy ,
3438 Download ,
3539 Github ,
3640 GitlabIcon as GitLab ,
41+ Info ,
3742 Maximize ,
3843 Minimize ,
3944 RefreshCw ,
@@ -110,6 +115,9 @@ export default function RepoProjectStructure() {
110115 message : "" ,
111116 isError : false ,
112117 } )
118+ const [ repoValidation , setRepoValidation ] = useState < RepoValidationResult | null > ( null )
119+ const [ showValidationDialog , setShowValidationDialog ] = useState ( false )
120+ const [ proceedWithLargeRepo , setProceedWithLargeRepo ] = useState ( false )
113121 const [ copied , setCopied ] = useState ( false )
114122 const [ expanded , setExpanded ] = useState ( false )
115123 const [ viewMode , setViewMode ] = useState < "ascii" | "interactive" > ( "ascii" )
@@ -145,7 +153,7 @@ export default function RepoProjectStructure() {
145153 )
146154
147155 const handleFetchStructure = useCallback (
148- async ( url : string = repoUrl ) => {
156+ async ( url : string = repoUrl , skipValidation : boolean = false ) => {
149157 if ( ! url ) {
150158 setValidation ( { message : "Repository URL is required" , isError : true } )
151159 return
@@ -161,7 +169,22 @@ export default function RepoProjectStructure() {
161169
162170 setLoading ( true )
163171 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+
165188 const map = generateStructure ( tree )
166189 setStructureMap ( map )
167190 setValidation ( { message : "" , isError : false } )
@@ -170,6 +193,10 @@ export default function RepoProjectStructure() {
170193 const { fileTypes, languages } = analyzeRepository ( map )
171194 setFileTypeData ( fileTypes )
172195 setLanguageData ( languages )
196+
197+ // Reset validation dialog state
198+ setShowValidationDialog ( false )
199+ setProceedWithLargeRepo ( false )
173200 } catch ( err : unknown ) {
174201 if ( err instanceof Error ) {
175202 console . error ( err )
@@ -184,12 +211,19 @@ export default function RepoProjectStructure() {
184211 isError : true ,
185212 } )
186213 }
214+ setRepoValidation ( null )
187215 }
188216 setLoading ( false )
189217 } ,
190- [ repoUrl , repoType ] ,
218+ [ repoUrl , repoType , proceedWithLargeRepo ] ,
191219 )
192220
221+ const handleProceedWithLargeRepo = useCallback ( ( ) => {
222+ setProceedWithLargeRepo ( true )
223+ setShowValidationDialog ( false )
224+ handleFetchStructure ( repoUrl , true )
225+ } , [ repoUrl , handleFetchStructure ] )
226+
193227 useEffect ( ( ) => {
194228 const savedUrl = localStorage . getItem ( "lastRepoUrl" )
195229 if ( savedUrl ) {
@@ -219,17 +253,21 @@ export default function RepoProjectStructure() {
219253 }
220254 } , [ ] )
221255
256+ // Memoized filtering with performance optimization
222257 const filterStructure = useCallback ( ( map : DirectoryMap , term : string ) : DirectoryMap => {
258+ if ( ! term . trim ( ) ) return map
259+
223260 const filteredMap : DirectoryMap = new Map ( )
261+ const lowerTerm = term . toLowerCase ( )
224262
225263 for ( const [ key , value ] of map . entries ( ) ) {
226264 if ( value && typeof value === "object" && "type" in value && value . type === "file" ) {
227- if ( key . toLowerCase ( ) . includes ( term . toLowerCase ( ) ) ) {
265+ if ( key . toLowerCase ( ) . includes ( lowerTerm ) ) {
228266 filteredMap . set ( key , value )
229267 }
230268 } else if ( value instanceof Map ) {
231269 const filteredSubMap = filterStructure ( value , term )
232- if ( filteredSubMap . size > 0 || key . toLowerCase ( ) . includes ( term . toLowerCase ( ) ) ) {
270+ if ( filteredSubMap . size > 0 || key . toLowerCase ( ) . includes ( lowerTerm ) ) {
233271 filteredMap . set ( key , filteredSubMap )
234272 }
235273 }
@@ -243,10 +281,15 @@ export default function RepoProjectStructure() {
243281 [ filterStructure , structureMap , searchTerm ] ,
244282 )
245283
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 ] )
250293
251294 const copyToClipboard = useCallback ( ( ) => {
252295 navigator . clipboard . writeText ( customizedStructure ) . then ( ( ) => {
@@ -259,6 +302,7 @@ export default function RepoProjectStructure() {
259302 setRepoUrl ( "" )
260303 localStorage . removeItem ( "lastRepoUrl" )
261304 setStructureMap ( new Map ( ) )
305+ setRepoValidation ( null )
262306 if ( inputRef . current ) {
263307 inputRef . current . focus ( )
264308 }
@@ -275,19 +319,19 @@ export default function RepoProjectStructure() {
275319
276320 switch ( format ) {
277321 case "md" :
278- content = `# Repository Structure\n\n\`\`\`\n${ customizedStructure } \`\`\``
322+ content = `# Directory Structure\n\n\`\`\`\n${ customizedStructure } \`\`\``
279323 mimeType = "text/markdown;charset=utf-8"
280324 fileName = "README.md"
281325 break
282326 case "txt" :
283327 content = customizedStructure
284328 mimeType = "text/plain;charset=utf-8"
285- fileName = "repository -structure.txt"
329+ fileName = "directory -structure.txt"
286330 break
287331 case "json" :
288332 content = JSON . stringify ( convertMapToJson ( filteredStructureMap ) , null , 2 )
289333 mimeType = "application/json;charset=utf-8"
290- fileName = "repository -structure.json"
334+ fileName = "directory -structure.json"
291335 break
292336 case "html" :
293337 content = `
@@ -305,7 +349,7 @@ export default function RepoProjectStructure() {
305349 </html>
306350 `
307351 mimeType = "text/html;charset=utf-8"
308- fileName = "repository -structure.html"
352+ fileName = "directory -structure.html"
309353 break
310354 }
311355
@@ -334,6 +378,79 @@ export default function RepoProjectStructure() {
334378
335379 return (
336380 < 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+
337454 < Card
338455 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"
339456 id = "generator"
@@ -351,6 +468,20 @@ export default function RepoProjectStructure() {
351468 { /* Token Status */ }
352469 < TokenStatus />
353470
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+
354485 < div className = "grid grid-cols-1 sm:grid-cols-12 gap-4 items-end" >
355486 { /* Repository Type Select */ }
356487 < div className = "sm:col-span-3" >
@@ -582,26 +713,28 @@ export default function RepoProjectStructure() {
582713 { /* Code Block */ }
583714 < div className = "relative border border-gray-300 dark:border-gray-600 border-t-0 rounded-b-lg overflow-hidden" ref = { treeRef } >
584715 { 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 >
603736 ) : 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" } } >
605738 < InteractiveTreeView structure = { filteredStructureMap } customizationOptions = { customizationOptions } />
606739 </ div >
607740 ) : (
0 commit comments