Skip to content

Commit 3d56a82

Browse files
committed
feat: optimize large repository handling with validation, performance, and UX improvements
1 parent 3a28f33 commit 3d56a82

File tree

2 files changed

+355
-108
lines changed

2 files changed

+355
-108
lines changed

src/app/generator/repo-tree-generator.tsx

Lines changed: 165 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
1414
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
1515
import { Input } from "@/components/ui/input"
1616
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
17+
import { Alert, AlertDescription } from "@/components/ui/alert"
1718
import {
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"
2629
import { convertMapToJson } from "@/lib/utils"
2730
import type { TreeCustomizationOptions } from "@/types/tree-customization"
2831
import { saveAs } from "file-saver"
2932
import {
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

Comments
 (0)