diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 61e7f5b..729866f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,8 +14,8 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { if (loading) { return ( -
-
Loading...
+
+
Loading...
); } diff --git a/frontend/src/components/AddRepoForm.tsx b/frontend/src/components/AddRepoForm.tsx index 7f01544..d0cf43a 100644 --- a/frontend/src/components/AddRepoForm.tsx +++ b/frontend/src/components/AddRepoForm.tsx @@ -1,6 +1,9 @@ import { useState } from 'react' import { motion, AnimatePresence } from 'framer-motion' -import { Package, Plus, X } from 'lucide-react' +import { Package, Plus, X, Loader2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' interface AddRepoFormProps { onAdd: (gitUrl: string, branch: string) => Promise @@ -23,16 +26,14 @@ export function AddRepoForm({ onAdd, loading }: AddRepoFormProps) { return ( <> - setShowForm(true)} - className="px-5 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white text-sm font-medium rounded-xl transition-all shadow-lg shadow-blue-500/20 flex items-center gap-2" disabled={loading} + className="bg-primary hover:bg-primary/90 text-primary-foreground gap-2" > - + - Add Repository - + + Add Repository + {showForm && ( @@ -40,7 +41,7 @@ export function AddRepoForm({ onAdd, loading }: AddRepoFormProps) { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50" + className="fixed inset-0 bg-background/80 backdrop-blur-md flex items-center justify-center z-50" onClick={() => !loading && setShowForm(false)} > e.stopPropagation()} - className="bg-gradient-to-br from-[#111113] to-[#0a0a0c] border border-white/10 rounded-2xl shadow-2xl w-full max-w-md mx-4" + className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-md mx-4" > {/* Header */} -
+
-
- +
+
-

Add Repository

-

Clone and index with AI

+

Add Repository

+

Clone and index with AI

- +
diff --git a/frontend/src/components/CodebaseIntelligence.tsx b/frontend/src/components/CodebaseIntelligence.tsx new file mode 100644 index 0000000..9d67f59 --- /dev/null +++ b/frontend/src/components/CodebaseIntelligence.tsx @@ -0,0 +1,427 @@ +import { useMemo } from 'react' +import { motion } from 'framer-motion' +import { + Brain, FileCode, GitBranch, Folder, Search, + ArrowRight, AlertTriangle, CheckCircle2, Sparkles, + Target, Layers, Zap +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { useDependencyGraph, useStyleAnalysis } from '../hooks/useCachedQuery' +import type { Repository } from '../types' + +interface CodebaseIntelligenceProps { + repo: Repository + apiKey: string + onTabChange?: (tab: string) => void +} + +interface ArchitectureNode { + path: string + name: string + type: 'file' | 'dir' + annotation?: string + dependents?: number + children?: ArchitectureNode[] +} + +export function CodebaseIntelligence({ repo, apiKey, onTabChange }: CodebaseIntelligenceProps) { + const { data: deps, isLoading: depsLoading } = useDependencyGraph({ repoId: repo.id, apiKey }) + const { data: style, isLoading: styleLoading } = useStyleAnalysis({ repoId: repo.id, apiKey }) + + const isLoading = depsLoading || styleLoading + + // Derive intelligence from raw data + const intelligence = useMemo(() => { + const nodes = deps?.nodes || [] + const edges = deps?.edges || [] + + // Gate on deps existence or having graph data + if (!deps && nodes.length === 0) return null + + // Calculate in-degree and out-degree from edges + const inDegree: Record = {} + const outDegree: Record = {} + + edges.forEach((e: any) => { + inDegree[e.target] = (inDegree[e.target] || 0) + 1 + outDegree[e.source] = (outDegree[e.source] || 0) + 1 + }) + + // Critical files: highest in-degree (most dependents) + const criticalFiles = Object.entries(inDegree) + .map(([file, dependents]) => ({ file, dependents })) + .sort((a, b) => b.dependents - a.dependents) + .slice(0, 5) + + // Entry points: imported by many, imports few (high in-degree, low out-degree) + const entryPoints = nodes + .map((n: any) => ({ + file: n.id, + name: n.label, + importedBy: inDegree[n.id] || 0, + imports: outDegree[n.id] || 0, + score: (inDegree[n.id] || 0) - (outDegree[n.id] || 0) + })) + .filter((n: any) => n.importedBy > 0) + .sort((a: any, b: any) => b.score - a.score) + .slice(0, 3) + + // Detect primary language + const langCount: Record = {} + nodes.forEach((n: any) => { + if (n.language && n.language !== 'unknown') { + langCount[n.language] = (langCount[n.language] || 0) + 1 + } + }) + const primaryLang = Object.entries(langCount).sort((a, b) => b[1] - a[1])[0]?.[0] || 'code' + + // Build simple architecture tree from file paths + const dirs = new Map() + nodes.forEach((n: any) => { + const parts = n.id.split('/') + if (parts.length > 1) { + const dir = parts[0] + const existing = dirs.get(dir) || { count: 0, critical: false } + existing.count++ + if (criticalFiles.some((c: any) => c.file.startsWith(dir + '/'))) { + existing.critical = true + } + dirs.set(dir, existing) + } + }) + + // Find "start here" file - the most critical file that's also an entry point + const criticalEntryPoint = criticalFiles.find(cf => + entryPoints.some(ep => ep.file === cf.file) + ) + const startHere = criticalEntryPoint?.file || entryPoints[0]?.file || null + + // Generate smart summary + const totalFiles = deps?.total_files || nodes.length + const totalFunctions: number | null = style?.summary?.total_functions ?? null + + let sizeDesc: string | null = null + if (typeof totalFunctions === 'number') { + if (totalFunctions > 1000) sizeDesc = 'large' + else if (totalFunctions > 200) sizeDesc = 'medium-sized' + else sizeDesc = 'compact' + } + + // Detect patterns + const hasMiddleware = nodes.some((n: any) => + n.id.includes('middleware') || n.id.includes('plugin') + ) + const hasHooks = nodes.some((n: any) => + n.id.includes('hooks') || n.id.includes('use') + ) + + return { + summary: { + size: sizeDesc, + language: primaryLang, + totalFiles, + totalFunctions, + pattern: hasMiddleware ? 'middleware/plugin architecture' : + hasHooks ? 'hooks-based architecture' : null + }, + entryPoints, + criticalFiles: criticalFiles.slice(0, 3), + startHere, + architecture: Array.from(dirs.entries()) + .sort((a, b) => b[1].count - a[1].count) + .slice(0, 6) + .map(([dir, info]) => ({ + dir, + count: info.count, + critical: info.critical + })), + health: { + typeHints: style?.summary?.type_hints_usage || null, + asyncAdoption: style?.summary?.async_adoption || null, + namingConsistency: style?.naming_conventions?.functions ? + Object.values(style.naming_conventions.functions as Record) + .reduce((max: number, v: any) => Math.max(max, parseFloat(v.percentage) || 0), 0) : null + } + } + }, [deps, style, repo]) + + if (isLoading) { + return + } + + if (!intelligence) { + return null + } + + return ( +
+ {/* Hero: Codebase Intelligence */} + +
+
+ +
+ +
+

+ Codebase Intelligence + + AI-analyzed + +

+ +

+ {repo.name} is a {intelligence.summary.size ? `${intelligence.summary.size} ` : ''}{intelligence.summary.language} codebase + {intelligence.summary.totalFunctions != null ? ( + <> with {intelligence.summary.totalFunctions.toLocaleString()} functions + ) : null} + {' '}across {intelligence.summary.totalFiles} files. + {intelligence.summary.pattern && ( + <> Uses {intelligence.summary.pattern}. + )} + {intelligence.startHere && ( + <> Core logic centers around + {intelligence.startHere.split('/').pop()} + . + )} +

+ + {/* Quick Actions */} +
+ {intelligence.startHere && ( + + )} + + +
+
+
+
+ + {/* Two Column Layout */} +
+ {/* Critical Files */} + +

+ + High-Impact Files +

+

+ Changes here affect the most code. Handle with care. +

+
+ {intelligence.criticalFiles.map((file: any, idx: number) => ( +
+
+ + + {file.file} + +
+ + {file.dependents} deps + +
+ ))} +
+
+ + {/* Architecture Overview */} + +

+ + Architecture +

+

+ Directory structure with file counts. +

+
+ {intelligence.architecture.map((item: any) => ( +
+
+ + {item.dir}/ + {item.critical && ( + + core + + )} +
+ + {item.count} files + +
+ ))} +
+
+
+ + {/* Health & Entry Points Row */} +
+ {/* Entry Points */} + {intelligence.entryPoints.length > 0 && ( + +

+ + Entry Points +

+

+ Main API surface — most imported files. +

+
+ {intelligence.entryPoints.map((entry: any) => ( +
+ + {entry.file} + + + ← {entry.importedBy} imports + +
+ ))} +
+
+ )} + + {/* Health Indicators */} + +

+ + Code Health +

+
+ + + +
+

Branch

+

+ + {repo.branch} +

+
+
+
+
+
+ ) +} + +function HealthIndicator({ label, value, threshold }: { label: string, value: string | number | null, threshold: number }) { + if (value == null) { + return ( +
+

{label}

+

N/A

+
+ ) + } + + const numValue = typeof value === 'number' ? value : parseFloat(value) + const isGood = !isNaN(numValue) && numValue >= threshold + const displayValue = typeof value === 'number' ? `${value}%` : value + + return ( +
+

{label}

+

+ {isGood && } + {displayValue} +

+
+ ) +} + +function IntelligenceSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) +} diff --git a/frontend/src/components/DependencyGraph.tsx b/frontend/src/components/DependencyGraph.tsx index f788134..2b60f9f 100644 --- a/frontend/src/components/DependencyGraph.tsx +++ b/frontend/src/components/DependencyGraph.tsx @@ -55,23 +55,14 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps const [allNodes, setAllNodes] = useState([]) const [allEdges, setAllEdges] = useState([]) - // Use cached query for dependencies - const { data, isLoading: loading, isFetching } = useDependencyGraph({ - repoId, - apiKey - }) + const { data, isLoading: loading, isFetching } = useDependencyGraph({ repoId, apiKey }) - // Process data when it arrives useEffect(() => { - if (data) { - processGraphData(data) - } + if (data) processGraphData(data) }, [data]) useEffect(() => { - if (allNodes.length > 0) { - applyFilters() - } + if (allNodes.length > 0) applyFilters() }, [filterCritical, minDeps, allNodes, allEdges]) const applyFilters = () => { @@ -81,19 +72,13 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps if (filterCritical || minDeps > 0) { const threshold = minDeps || 3 filteredNodes = allNodes.filter((node: any) => - (node.data.imports || 0) >= threshold || - allEdges.some(e => e.target === node.id) + (node.data.imports || 0) >= threshold || allEdges.some(e => e.target === node.id) ) - const nodeIds = new Set(filteredNodes.map(n => n.id)) - filteredEdges = allEdges.filter(e => - nodeIds.has(e.source) && nodeIds.has(e.target) - ) + filteredEdges = allEdges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target)) } - const { nodes: layoutedNodes, edges: layoutedEdges } = - getLayoutedElements(filteredNodes, filteredEdges) - + const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(filteredNodes, filteredEdges) setNodes(layoutedNodes) setEdges(layoutedEdges) } @@ -110,14 +95,8 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps data: { label: (
-
- {fileName} -
- {importCount > 0 && ( -
- {importCount} imports -
- )} +
{fileName}
+ {importCount > 0 &&
{importCount} imports
}
), language: node.language, @@ -127,7 +106,7 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps style: { background: getLanguageColor(node.language), color: 'white', - border: '2px solid #3b82f6', + border: '2px solid hsl(var(--primary))', borderRadius: '8px', padding: '8px 12px', fontSize: '11px', @@ -143,89 +122,62 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps source: edge.source, target: edge.target, animated: false, - style: { stroke: '#4b5563', strokeWidth: 1.5 }, - markerEnd: { - type: MarkerType.ArrowClosed, - color: '#4b5563', - }, + style: { stroke: '#6b7280', strokeWidth: 1.5 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#6b7280' }, })) setAllNodes(flowNodes) setAllEdges(flowEdges) setMetrics(data.metrics) - const { nodes: layoutedNodes, edges: layoutedEdges } = - getLayoutedElements(flowNodes, flowEdges) - + const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(flowNodes, flowEdges) setNodes(layoutedNodes) setEdges(layoutedEdges) } const handleNodeClick = useCallback((event: any, node: Node) => { setHighlightedNode(node.id) - - const connectedNodeIds = new Set() - connectedNodeIds.add(node.id) - + const connectedNodeIds = new Set([node.id]) allEdges.forEach(edge => { if (edge.source === node.id) connectedNodeIds.add(edge.target) if (edge.target === node.id) connectedNodeIds.add(edge.source) }) - setNodes(nodes => - nodes.map(n => ({ - ...n, - style: { - ...n.style, - opacity: connectedNodeIds.has(n.id) ? 1 : 0.3, - border: n.id === node.id ? '3px solid #ef4444' : n.style?.border - } - })) - ) + setNodes(nodes => nodes.map(n => ({ + ...n, + style: { + ...n.style, + opacity: connectedNodeIds.has(n.id) ? 1 : 0.3, + border: n.id === node.id ? '3px solid #ef4444' : n.style?.border + } + }))) - setEdges(edges => - edges.map(e => ({ - ...e, - style: { - ...e.style, - opacity: e.source === node.id || e.target === node.id ? 1 : 0.1, - strokeWidth: e.source === node.id || e.target === node.id ? 2.5 : 1.5 - } - })) - ) + setEdges(edges => edges.map(e => ({ + ...e, + style: { + ...e.style, + opacity: e.source === node.id || e.target === node.id ? 1 : 0.1, + strokeWidth: e.source === node.id || e.target === node.id ? 2.5 : 1.5 + } + }))) }, [allEdges]) const resetHighlight = () => { setHighlightedNode(null) - setNodes(nodes => - nodes.map(n => ({ - ...n, - style: { ...n.style, opacity: 1, border: '2px solid #3b82f6' } - })) - ) - setEdges(edges => - edges.map(e => ({ - ...e, - style: { ...e.style, opacity: 1, strokeWidth: 1.5 } - })) - ) + setNodes(nodes => nodes.map(n => ({ ...n, style: { ...n.style, opacity: 1, border: '2px solid hsl(var(--primary))' } }))) + setEdges(edges => edges.map(e => ({ ...e, style: { ...e.style, opacity: 1, strokeWidth: 1.5 } }))) } const getLanguageColor = (language: string) => { - const colors: any = { - 'python': '#3776ab', - 'javascript': '#f7df1e', - 'typescript': '#3178c6', - 'unknown': '#6b7280' - } + const colors: any = { 'python': '#3776ab', 'javascript': '#f7df1e', 'typescript': '#3178c6', 'unknown': '#6b7280' } return colors[language] || colors.unknown } if (loading) { return (
-
-

Building dependency graph...

+
+

Building dependency graph...

) } @@ -234,57 +186,45 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps
{/* Metrics */}
-
-
Total Files
-
{allNodes.length}
+
+
Total Files
+
{allNodes.length}
-
-
Dependencies
-
{edges.length}
+
+
Dependencies
+
{edges.length}
-
-
Avg per File
-
- {metrics?.avg_dependencies?.toFixed(1) || 0} -
+
+
Avg per File
+
{metrics?.avg_dependencies?.toFixed(1) || 0}
-
-
Showing
-
{nodes.length}
+
+
Showing
+
{nodes.length}
{/* Filter Controls */} -
+
- - setMinDeps(Number(e.target.value))} - className="w-32 accent-blue-500" - /> - {minDeps} + + setMinDeps(Number(e.target.value))} className="w-32 accent-primary" /> + {minDeps}
{highlightedNode && ( - )} @@ -293,17 +233,13 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps {/* Most Critical Files */} {metrics?.most_critical_files && metrics.most_critical_files.length > 0 && ( -
-

Most Critical Files

+
+

Most Critical Files

{metrics.most_critical_files.slice(0, 5).map((item: any, idx: number) => (
- - {item.file.split('/').slice(-2).join('/')} - - - {item.dependents} dependents - + {item.file.split('/').slice(-2).join('/')} + {item.dependents} dependents
))}
@@ -311,7 +247,7 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps )} {/* Graph Visualization */} -
+
- - + + { - const style = node.style as any - return style?.background || '#6b7280' - }} + nodeColor={(node) => (node.style as any)?.background || '#6b7280'} maskColor="rgba(0, 0, 0, 0.5)" - className="!bg-[#111113] !border-white/10" + className="!bg-card !border-border" />
{/* Legend */} -
-

Graph Legend

+
+

Graph Legend

- Python + Python
- TypeScript + TypeScript
- JavaScript + JavaScript
-
- Dependency +
+ Dependency
-
- +
+ Click any node to highlight its dependencies • Drag to pan • Scroll to zoom
diff --git a/frontend/src/components/ImpactAnalyzer.tsx b/frontend/src/components/ImpactAnalyzer.tsx index a656d0e..b748aeb 100644 --- a/frontend/src/components/ImpactAnalyzer.tsx +++ b/frontend/src/components/ImpactAnalyzer.tsx @@ -1,4 +1,7 @@ import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' interface ImpactAnalyzerProps { repoId: string @@ -60,47 +63,45 @@ export function ImpactAnalyzer({ repoId, apiUrl, apiKey }: ImpactAnalyzerProps) const getRiskColor = (risk: string) => { switch (risk) { - case 'high': return 'text-red-400 bg-red-500/10 border-red-500/20' - case 'medium': return 'text-yellow-400 bg-yellow-500/10 border-yellow-500/20' - case 'low': return 'text-green-400 bg-green-500/10 border-green-500/20' - default: return 'text-gray-400 bg-white/5 border-white/10' + case 'high': return 'text-destructive bg-destructive/10 border-destructive/20' + case 'medium': return 'text-yellow-500 dark:text-yellow-400 bg-yellow-500/10 border-yellow-500/20' + case 'low': return 'text-green-500 dark:text-green-400 bg-green-500/10 border-green-500/20' + default: return 'text-muted-foreground bg-muted border-border' } } return (
{/* Input Form */} -
-

Analyze Change Impact

+
+

Analyze Change Impact

-
- - + + setFilePath(e.target.value)} placeholder="e.g., src/auth/middleware.py or components/Button.tsx" - className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-gray-500 focus:outline-none focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/20 transition-all" disabled={loading} /> -

+

Enter the path of the file you want to modify to see its impact

- +
{error && ( -
+
{error}
)} @@ -110,66 +111,66 @@ export function ImpactAnalyzer({ repoId, apiUrl, apiKey }: ImpactAnalyzerProps) {result && (
{/* Risk Assessment */} -
+
-

Risk Assessment

+

Risk Assessment

{result.risk_level} Risk
-

+

{result.file}

-

+

{result.impact_summary}

{/* Impact Overview */}
-
-
Direct Dependencies
-
+
+
Direct Dependencies
+
{result.dependency_count}
-
+
Files this imports
-
-
Total Impact
-
+
+
Total Impact
+
{result.dependent_count}
-
+
Files affected by changes
-
-
Test Files
-
+
+
Test Files
+
{result.test_files?.length || 0}
-
+
Related test coverage
- {/* Dependencies (What This File Needs) */} + {/* Dependencies */} {result.direct_dependencies && result.direct_dependencies.length > 0 && ( -
-

+
+

Dependencies ({result.direct_dependencies.length})

-

+

Files this file imports (upstream)

{result.direct_dependencies.map((dep, idx) => ( -
+
{dep}
))} @@ -177,18 +178,18 @@ export function ImpactAnalyzer({ repoId, apiUrl, apiKey }: ImpactAnalyzerProps)
)} - {/* Dependents (What Breaks If Changed) */} + {/* Dependents */} {result.all_dependents && result.all_dependents.length > 0 && ( -
-

+
+

Affected Files ({result.all_dependents.length})

-

+

Files that would be impacted by changes to this file (downstream)

{result.all_dependents.map((dep, idx) => ( -
+
{dep}
))} @@ -198,16 +199,16 @@ export function ImpactAnalyzer({ repoId, apiUrl, apiKey }: ImpactAnalyzerProps) {/* Test Files */} {result.test_files && result.test_files.length > 0 && ( -
-

+
+

Related Tests ({result.test_files.length})

-

+

Test files that may need updates

{result.test_files.map((test, idx) => ( -
+
{test}
))} diff --git a/frontend/src/components/RepoList.tsx b/frontend/src/components/RepoList.tsx index 14b241a..2c4ae86 100644 --- a/frontend/src/components/RepoList.tsx +++ b/frontend/src/components/RepoList.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useMemo } from 'react' import { motion } from 'framer-motion' +import { FolderGit2, Plus } from 'lucide-react' import type { Repository } from '../types' import { RepoGridSkeleton } from './ui/Skeleton' @@ -16,11 +17,11 @@ const StatusBadge = ({ status }: { status: string }) => { return ( - + {isIndexed ? 'Indexed' : 'Pending'} ) @@ -50,16 +51,16 @@ const RepoCard = ({ repo, index, onSelect }: { }} onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)} - className="group relative text-left rounded-2xl overflow-hidden w-full - bg-[#111113] border border-white/[0.06] hover:border-blue-500/40 - focus:outline-none focus:ring-2 focus:ring-blue-500/50 p-5 transition-colors" + className="group relative text-left rounded-xl overflow-hidden w-full + bg-card border border-border hover:border-primary/40 + focus:outline-none focus:ring-2 focus:ring-primary/50 p-5 transition-colors" > - {/* Mouse glow */} + {/* Mouse glow effect */} {hovering && (
)} @@ -67,25 +68,23 @@ const RepoCard = ({ repo, index, onSelect }: {
{/* Header */}
-
- - - +
+
{/* Title */} -

+

{repo.name}

-

{repo.branch}

+

{repo.branch}

{/* Stats */} -
+
- Functions - + Files + {(repo.file_count || 0).toLocaleString()}
@@ -103,22 +102,19 @@ export function RepoList({ repos, selectedRepo, onSelect, loading }: RepoListPro -
- - - +
+
-

No repositories yet

-

+

No repositories yet

+

Add your first repository to start searching code with AI

) } - // Sort: indexed first, then by function count desc const sortedRepos = useMemo(() => { return [...repos].sort((a, b) => { if (a.status === 'indexed' && b.status !== 'indexed') return -1 diff --git a/frontend/src/components/RepoOverview.tsx b/frontend/src/components/RepoOverview.tsx index 670a1e2..ba02d11 100644 --- a/frontend/src/components/RepoOverview.tsx +++ b/frontend/src/components/RepoOverview.tsx @@ -2,6 +2,8 @@ import { useState, useEffect, useRef } from 'react' import { motion } from 'framer-motion' import { toast } from 'sonner' import { Progress } from '@/components/ui/progress' +import { Button } from '@/components/ui/button' +import { CodebaseIntelligence } from './CodebaseIntelligence' import type { Repository } from '../types' import { WS_URL } from '../config/api' import { useInvalidateRepoCache } from '../hooks/useCachedQuery' @@ -11,6 +13,7 @@ interface RepoOverviewProps { onReindex: () => void apiUrl: string apiKey: string + onTabChange?: (tab: string) => void } interface IndexProgress { @@ -20,7 +23,7 @@ interface IndexProgress { progress_pct: number } -export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewProps) { +export function RepoOverview({ repo, onReindex, apiUrl, apiKey, onTabChange }: RepoOverviewProps) { const [indexing, setIndexing] = useState(false) const [progress, setProgress] = useState(null) const wsRef = useRef(null) @@ -83,67 +86,47 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr return (
- {/* Stats Grid */} -
- {/* Status */} - -

Status

-
- - {repo.status === 'indexed' ? 'Indexed' : 'Pending'} -
-
+ {/* Codebase Intelligence - Hero Section */} + {repo.status === 'indexed' && ( + + )} - {/* Functions */} + {/* Pending State */} + {repo.status !== 'indexed' && ( -

Functions Indexed

-

- {(repo.file_count || 0).toLocaleString()} +

+ +
+

Repository Not Indexed

+

+ Index this repository to unlock codebase intelligence, semantic search, and dependency analysis.

+
- - {/* Branch */} - -

Branch

-

{repo.branch}

-
-
+ )} {/* Indexing Progress */} {indexing && progress && (
- - Indexing in Progress + + Indexing in Progress
- {progress.progress_pct}% + {progress.progress_pct}%
-
+
Files: {progress.files_processed}/{progress.total_files || '?'} Functions: {progress.functions_indexed}
@@ -155,23 +138,23 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }} - className="bg-[#0a0a0c] border border-white/[0.06] rounded-xl p-5" + className="bg-muted border border-border rounded-xl p-5" > -

Repository Details

+

Repository Details

- Name - {repo.name} + Name + {repo.name}
- Local Path - {repo.local_path} + Local Path + {repo.local_path}
@@ -181,26 +164,26 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.4 }} - className="bg-[#0a0a0c] border border-white/[0.06] rounded-xl p-5" + className="bg-muted border border-border rounded-xl p-5" > -

Actions

-

+

Actions

+

Re-indexing uses incremental mode — only processes changed files.

- - +
diff --git a/frontend/src/components/RepoSummaryCard.tsx b/frontend/src/components/RepoSummaryCard.tsx new file mode 100644 index 0000000..fb15021 --- /dev/null +++ b/frontend/src/components/RepoSummaryCard.tsx @@ -0,0 +1,176 @@ +import { motion } from 'framer-motion' +import { Sparkles, GitBranch, Code2, FileCode, AlertTriangle } from 'lucide-react' +import { useRepoInsights, useStyleAnalysis } from '../hooks/useCachedQuery' +import type { Repository } from '../types' + +interface RepoSummaryCardProps { + repo: Repository + apiKey: string +} + +export function RepoSummaryCard({ repo, apiKey }: RepoSummaryCardProps) { + const { data: insights, isLoading: insightsLoading } = useRepoInsights({ repoId: repo.id, apiKey }) + const { data: style, isLoading: styleLoading } = useStyleAnalysis({ repoId: repo.id, apiKey }) + + const isLoading = insightsLoading || styleLoading + + if (isLoading) { + return ( + +
+
+
+
+
+
+
+
+ + ) + } + + // Generate summary from insights + style data + const summary = generateSummary(repo, insights, style) + + return ( + +
+
+ +
+ +
+

+ AI Summary + Auto-generated +

+ +

+ {repo.name} {summary.main} +

+ + {/* Quick Stats Pills */} +
+ {summary.stats.map((stat, idx) => ( +
+ + {stat.label}: + {stat.value} +
+ ))} +
+ + {/* Critical Files Warning */} + {summary.criticalFiles && summary.criticalFiles.length > 0 && ( +
+
+ + High-impact files (most dependencies) +
+
+ {summary.criticalFiles.slice(0, 3).map((file, idx) => ( + + {file} + + ))} +
+
+ )} +
+
+
+ ) +} + +function generateSummary(repo: Repository, insights: any, style: any) { + const stats: { icon: any; label: string; value: string }[] = [] + const criticalFiles: string[] = [] + + // Extract key metrics + const fileCount = insights?.total_files || repo.file_count || 0 + const functionCount = style?.summary?.total_functions || insights?.total_functions || 0 + const languages = insights?.languages || style?.language_distribution || {} + + // Get primary language by sorting entries by value descending + const langEntries = Object.entries(languages) + let primaryLang = 'Unknown' + if (langEntries.length > 0) { + const sorted = langEntries.sort((a, b) => { + const valA = typeof a[1] === 'number' ? a[1] : (a[1] as any)?.percentage ?? (a[1] as any)?.count ?? 0 + const valB = typeof b[1] === 'number' ? b[1] : (b[1] as any)?.percentage ?? (b[1] as any)?.count ?? 0 + return Number(valB) - Number(valA) + }) + primaryLang = sorted[0][0] + } + + const asyncAdoption = style?.summary?.async_adoption || null + const typeHints = style?.summary?.type_hints_usage || null + + // Get naming convention - defensive parsing + const namingConventions = style?.naming_conventions?.functions || {} + const namingEntries = Object.entries(namingConventions) + const primaryNaming = namingEntries.length > 0 + ? namingEntries.sort((a: any, b: any) => { + const pctA = parseFloat(a[1]?.percentage ?? '0') || 0 + const pctB = parseFloat(b[1]?.percentage ?? '0') || 0 + return pctB - pctA + })[0] + : null + const namingStyle = primaryNaming ? primaryNaming[0] : null + + // Get critical files - defensive checks for malformed data + if (Array.isArray(insights?.most_critical_files)) { + insights.most_critical_files.slice(0, 3).forEach((f: any) => { + if (!f || typeof f.file !== 'string') return + criticalFiles.push(f.file.split('/').pop() || f.file) + }) + } + + // Build stats array + stats.push({ icon: FileCode, label: 'Files', value: fileCount.toLocaleString() }) + stats.push({ icon: Code2, label: 'Functions', value: functionCount.toLocaleString() }) + stats.push({ icon: GitBranch, label: 'Branch', value: repo.branch }) + + if (asyncAdoption && asyncAdoption !== '0%') { + stats.push({ icon: Code2, label: 'Async', value: asyncAdoption }) + } + + // Generate main summary text (repo name rendered separately in JSX) + let main = `is a ` + + if (functionCount > 500) main += 'large ' + else if (functionCount > 100) main += 'medium-sized ' + else main += 'compact ' + + main += `${primaryLang} codebase with ${functionCount.toLocaleString()} functions across ${fileCount} files. ` + + if (namingStyle && primaryNaming) { + main += `The code primarily uses ${namingStyle} naming conventions` + const consistencyPct = parseFloat((primaryNaming[1] as any)?.percentage ?? '0') || 0 + if (consistencyPct > 80) { + main += ` (${consistencyPct}% consistency)` + } + main += '. ' + } + + if (typeHints && typeHints !== '0%' && parseFloat(typeHints) > 50) { + main += `Strong type coverage at ${typeHints}. ` + } + + if (criticalFiles.length > 0) { + main += `Core architecture centers around ${criticalFiles[0]}.` + } + + return { main, stats, criticalFiles } +} diff --git a/frontend/src/components/SearchPanel.tsx b/frontend/src/components/SearchPanel.tsx index ab91e0d..998d962 100644 --- a/frontend/src/components/SearchPanel.tsx +++ b/frontend/src/components/SearchPanel.tsx @@ -9,9 +9,10 @@ interface SearchPanelProps { apiUrl: string; apiKey: string; repoUrl?: string; + defaultBranch?: string; } -export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl }: SearchPanelProps) { +export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }: SearchPanelProps) { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); @@ -22,7 +23,6 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl }: SearchPanelProp const handleSearch = async () => { if (!query.trim()) return; - setLoading(true); setHasSearched(true); setAiSummary(null); @@ -31,30 +31,17 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl }: SearchPanelProp try { const response = await fetch(`${apiUrl}/search`, { method: 'POST', - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query, - repo_id: repoId, - max_results: 10, - }), + headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, repo_id: repoId, max_results: 10 }), }); - const data = await response.json(); setResults(data.results || []); setSearchTime(Date.now() - startTime); setCached(data.cached || false); - - if (data.ai_summary) { - setAiSummary(data.ai_summary); - } + if (data.ai_summary) setAiSummary(data.ai_summary); } catch (error) { console.error('Search error:', error); - toast.error('Search failed', { - description: 'Please check your query and try again', - }); + toast.error('Search failed', { description: 'Please check your query and try again' }); } finally { setLoading(false); } @@ -63,30 +50,19 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl }: SearchPanelProp return (
{/* Search Box */} -
- +
+ {searchTime !== null && ( -
- - {results.length} results - - - - {searchTime}ms - +
+ {results.length} results + + {searchTime}ms {cached && ( <> - - - - Cached + + + Cached )} @@ -104,20 +80,19 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl }: SearchPanelProp isExpanded={idx === 0} aiSummary={idx === 0 ? aiSummary || undefined : undefined} repoUrl={repoUrl} + defaultBranch={defaultBranch} /> ))}
{/* Empty State */} {results.length === 0 && hasSearched && !loading && ( -
-
- +
+
+
-

No results found

-

- Try a different query or check if the repository is fully indexed -

+

No results found

+

Try a different query or check if the repository is fully indexed

)}
diff --git a/frontend/src/components/StyleInsights.tsx b/frontend/src/components/StyleInsights.tsx index 7b5abd2..99ebb45 100644 --- a/frontend/src/components/StyleInsights.tsx +++ b/frontend/src/components/StyleInsights.tsx @@ -7,14 +7,13 @@ interface StyleInsightsProps { } export function StyleInsights({ repoId, apiUrl, apiKey }: StyleInsightsProps) { - // Use cached query for style analysis const { data, isLoading: loading } = useStyleAnalysis({ repoId, apiKey }) if (loading) { return (
-
-

Analyzing code style patterns...

+
+

Analyzing code style patterns...

) } @@ -25,30 +24,30 @@ export function StyleInsights({ repoId, apiUrl, apiKey }: StyleInsightsProps) {
{/* Summary Cards */}
-
-
Files Analyzed
-
+
+
Files Analyzed
+
{data.summary?.total_files_analyzed || 0}
-
-
Functions
-
+
+
Functions
+
{data.summary?.total_functions || 0}
-
-
Async Adoption
-
+
+
Async Adoption
+
{data.summary?.async_adoption || '0%'}
-
-
Type Hints
-
+
+
Type Hints
+
{data.summary?.type_hints_usage || '0%'}
@@ -56,20 +55,20 @@ export function StyleInsights({ repoId, apiUrl, apiKey }: StyleInsightsProps) { {/* Naming Conventions */}
-
-

Function Naming

+
+

Function Naming

{data.naming_conventions?.functions && Object.entries(data.naming_conventions.functions).map(([convention, info]: any) => (
- + {convention}
- {info.count} - + {info.count} + {info.percentage}
@@ -79,20 +78,20 @@ export function StyleInsights({ repoId, apiUrl, apiKey }: StyleInsightsProps) {
-
-

Class Naming

+
+

Class Naming

{data.naming_conventions?.classes && Object.entries(data.naming_conventions.classes).map(([convention, info]: any) => (
- + {convention}
- {info.count} - + {info.count} + {info.percentage}
@@ -104,39 +103,39 @@ export function StyleInsights({ repoId, apiUrl, apiKey }: StyleInsightsProps) {
{/* Top Imports */} -
-

Most Common Imports

+
+

Most Common Imports

{data.top_imports?.slice(0, 10).map((item: any, idx: number) => (
- + {item.module} - {item.count}× + {item.count}×
))}
{/* Patterns */} -
-

Code Patterns

+
+

Code Patterns

-
- Async/Await Usage +
+ Async/Await Usage
- {data.patterns?.async_usage} - + {data.patterns?.async_usage} + {data.patterns?.async_percentage?.toFixed(0)}%
-
- Type Annotations +
+ Type Annotations
- {data.patterns?.type_annotations} - + {data.patterns?.type_annotations} + {data.patterns?.typed_percentage?.toFixed(0)}%
@@ -145,20 +144,20 @@ export function StyleInsights({ repoId, apiUrl, apiKey }: StyleInsightsProps) {
{/* Language Distribution */} -
-

Language Distribution

+
+

Language Distribution

{data.language_distribution && Object.entries(data.language_distribution).map(([lang, info]: any) => (
- {lang} -
+ {lang} +
- {info.percentage} + {info.percentage}
)) } diff --git a/frontend/src/components/dashboard/CommandPalette.tsx b/frontend/src/components/dashboard/CommandPalette.tsx index 59a71a0..2b396e1 100644 --- a/frontend/src/components/dashboard/CommandPalette.tsx +++ b/frontend/src/components/dashboard/CommandPalette.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef, useMemo } from 'react' import { useNavigate } from 'react-router-dom' +import { Search } from 'lucide-react' import { useAuth } from '../../contexts/AuthContext' import { API_URL } from '../../config/api' @@ -33,14 +34,10 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) { const navigate = useNavigate() const { session, signOut } = useAuth() - // Fetch repos for search useEffect(() => { - if (isOpen && session?.access_token) { - fetchRepos() - } + if (isOpen && session?.access_token) fetchRepos() }, [isOpen, session]) - // Focus input when opened useEffect(() => { if (isOpen) { setQuery('') @@ -49,10 +46,8 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) { } }, [isOpen]) - // Handle keyboard navigation useEffect(() => { if (!isOpen) return - const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'ArrowDown': @@ -76,7 +71,6 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) { break } } - window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [isOpen, selectedIndex, query]) @@ -93,11 +87,8 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) { } } - // Build command items const allItems: CommandItem[] = useMemo(() => { const items: CommandItem[] = [] - - // Repositories repos.forEach(repo => { items.push({ id: `repo-${repo.id}`, @@ -108,100 +99,26 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) { action: () => navigate(`/dashboard/repo/${repo.id}`) }) }) - - // Actions - items.push({ - id: 'action-add-repo', - type: 'action', - title: 'Add Repository', - subtitle: 'Clone and index a new repository', - icon: '➕', - action: () => { - // Trigger add repo modal - dispatch custom event - window.dispatchEvent(new CustomEvent('openAddRepo')) - navigate('/dashboard') - } - }) - - items.push({ - id: 'action-refresh', - type: 'action', - title: 'Refresh Repositories', - subtitle: 'Reload the repository list', - icon: '🔄', - action: () => { - window.location.reload() - } - }) - - // Navigation - items.push({ - id: 'nav-dashboard', - type: 'navigation', - title: 'Go to Dashboard', - subtitle: 'View all repositories', - icon: '🏠', - action: () => navigate('/dashboard') - }) - - items.push({ - id: 'nav-settings', - type: 'navigation', - title: 'Settings', - subtitle: 'Account and preferences', - icon: '⚙️', - action: () => navigate('/dashboard/settings') - }) - - items.push({ - id: 'nav-docs', - type: 'navigation', - title: 'Documentation', - subtitle: 'Learn how to use CodeIntel', - icon: '📚', - action: () => window.open('/docs', '_blank') - }) - - items.push({ - id: 'action-signout', - type: 'action', - title: 'Sign Out', - subtitle: 'Log out of your account', - icon: '🚪', - action: () => signOut() - }) - + items.push({ id: 'action-add-repo', type: 'action', title: 'Add Repository', subtitle: 'Clone and index a new repository', icon: '➕', action: () => { window.dispatchEvent(new CustomEvent('openAddRepo')); navigate('/dashboard') } }) + items.push({ id: 'action-refresh', type: 'action', title: 'Refresh Repositories', subtitle: 'Reload the repository list', icon: '🔄', action: () => window.location.reload() }) + items.push({ id: 'nav-dashboard', type: 'navigation', title: 'Go to Dashboard', subtitle: 'View all repositories', icon: '🏠', action: () => navigate('/dashboard') }) + items.push({ id: 'nav-settings', type: 'navigation', title: 'Settings', subtitle: 'Account and preferences', icon: '⚙️', action: () => navigate('/dashboard/settings') }) + items.push({ id: 'nav-docs', type: 'navigation', title: 'Documentation', subtitle: 'Learn how to use CodeIntel', icon: '📚', action: () => window.open('/docs', '_blank') }) + items.push({ id: 'action-signout', type: 'action', title: 'Sign Out', subtitle: 'Log out of your account', icon: '🚪', action: () => signOut() }) return items }, [repos, navigate, signOut]) - // Filter items based on query const filteredItems = useMemo(() => { if (!query.trim()) return allItems - const lowerQuery = query.toLowerCase() - return allItems.filter(item => - item.title.toLowerCase().includes(lowerQuery) || - item.subtitle?.toLowerCase().includes(lowerQuery) - ) + return allItems.filter(item => item.title.toLowerCase().includes(lowerQuery) || item.subtitle?.toLowerCase().includes(lowerQuery)) }, [allItems, query]) - // Reset selection when filter changes - useEffect(() => { - setSelectedIndex(0) - }, [query]) + useEffect(() => { setSelectedIndex(0) }, [query]) - // Group items by type const groupedItems = useMemo(() => { - const groups: { [key: string]: CommandItem[] } = { - repo: [], - action: [], - navigation: [] - } - - filteredItems.forEach(item => { - groups[item.type].push(item) - }) - + const groups: { [key: string]: CommandItem[] } = { repo: [], action: [], navigation: [] } + filteredItems.forEach(item => groups[item.type].push(item)) return groups }, [filteredItems]) @@ -209,108 +126,53 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) { return (
- {/* Backdrop */} -
+
- {/* Modal */} -
+
{/* Search Input */} -
- - - +
+ setQuery(e.target.value)} placeholder="Search commands, repos, actions..." - className="flex-1 bg-transparent text-white placeholder:text-gray-500 outline-none text-base" + className="flex-1 bg-transparent text-foreground placeholder:text-muted-foreground outline-none text-base" /> - - ESC - + ESC
{/* Results */}
{filteredItems.length === 0 ? ( -
- No results found for "{query}" -
+
No results found for "{query}"
) : ( <> - {/* Repositories */} {groupedItems.repo.length > 0 && (
-
- Repositories -
- {groupedItems.repo.map((item, idx) => { +
Repositories
+ {groupedItems.repo.map((item) => { const globalIndex = filteredItems.indexOf(item) - return ( - { - item.action() - onClose() - }} - onMouseEnter={() => setSelectedIndex(globalIndex)} - /> - ) + return { item.action(); onClose() }} onMouseEnter={() => setSelectedIndex(globalIndex)} /> })}
)} - - {/* Actions */} {groupedItems.action.length > 0 && (
-
- Actions -
+
Actions
{groupedItems.action.map((item) => { const globalIndex = filteredItems.indexOf(item) - return ( - { - item.action() - onClose() - }} - onMouseEnter={() => setSelectedIndex(globalIndex)} - /> - ) + return { item.action(); onClose() }} onMouseEnter={() => setSelectedIndex(globalIndex)} /> })}
)} - - {/* Navigation */} {groupedItems.navigation.length > 0 && (
-
- Navigation -
+
Navigation
{groupedItems.navigation.map((item) => { const globalIndex = filteredItems.indexOf(item) - return ( - { - item.action() - onClose() - }} - onMouseEnter={() => setSelectedIndex(globalIndex)} - /> - ) + return { item.action(); onClose() }} onMouseEnter={() => setSelectedIndex(globalIndex)} /> })}
)} @@ -319,58 +181,27 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
{/* Footer */} -
+
- - ↑↓ - navigate - - - - select - + ↑↓navigate + select
- CodeIntel + CodeIntel
) } -// Individual command item component -function CommandItem({ - item, - isSelected, - onClick, - onMouseEnter -}: { - item: CommandItem - isSelected: boolean - onClick: () => void - onMouseEnter: () => void -}) { +function CommandItemRow({ item, isSelected, onClick, onMouseEnter }: { item: CommandItem; isSelected: boolean; onClick: () => void; onMouseEnter: () => void }) { return ( - ) } diff --git a/frontend/src/components/dashboard/DashboardHome.tsx b/frontend/src/components/dashboard/DashboardHome.tsx index e2d5115..62a4c04 100644 --- a/frontend/src/components/dashboard/DashboardHome.tsx +++ b/frontend/src/components/dashboard/DashboardHome.tsx @@ -1,7 +1,16 @@ import { useState, useEffect } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { toast } from 'sonner' -import { LayoutDashboard, Search, GitFork, Code2, Zap } from 'lucide-react' +import { + LayoutDashboard, + Search, + GitFork, + Code2, + Zap, + ArrowLeft, + FolderGit2, + ExternalLink +} from 'lucide-react' import { useAuth } from '../../contexts/AuthContext' import { RepoList } from '../RepoList' import { AddRepoForm } from '../AddRepoForm' @@ -122,8 +131,8 @@ export function DashboardHome() { {/* Header */}
-

Repositories

-

+

Repositories

+

Semantic code search powered by AI

@@ -158,28 +167,27 @@ export function DashboardHome() {
-
- - - +
+
-

{selectedRepoData.name}

+

{selectedRepoData.name}

{selectedRepoData.git_url} +
@@ -187,15 +195,15 @@ export function DashboardHome() {
{/* Tabs */} -
+
{tabs.map(tab => (
{/* Tab Content */} -
+
{activeTab === 'overview' && ( setActiveTab(tab as RepoTab)} /> )} {activeTab === 'search' && ( @@ -220,6 +229,7 @@ export function DashboardHome() { apiUrl={API_URL} apiKey={session?.access_token || ''} repoUrl={selectedRepoData?.git_url?.replace('.git', '')} + defaultBranch={selectedRepoData?.branch} /> )} {activeTab === 'dependencies' && ( diff --git a/frontend/src/components/dashboard/DashboardLayout.tsx b/frontend/src/components/dashboard/DashboardLayout.tsx index 4257971..29af84b 100644 --- a/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/frontend/src/components/dashboard/DashboardLayout.tsx @@ -1,31 +1,49 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { Outlet } from 'react-router-dom' import { Sidebar } from './Sidebar' import { TopNav } from './TopNav' import { CommandPalette } from './CommandPalette' import { Toaster } from '@/components/ui/sonner' import { useKeyboardShortcut, SHORTCUTS } from '../../hooks/useKeyboardShortcut' +import { useTheme } from 'next-themes' interface DashboardLayoutProps { children?: React.ReactNode } +const SIDEBAR_STORAGE_KEY = 'codeintel-sidebar-collapsed' + export function DashboardLayout({ children }: DashboardLayoutProps) { - const [sidebarCollapsed, setSidebarCollapsed] = useState(false) + const { theme } = useTheme() + + const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { + try { + const stored = localStorage.getItem(SIDEBAR_STORAGE_KEY) + return stored ? JSON.parse(stored) : false + } catch { + return false + } + }) const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) - // Keyboard shortcuts + useEffect(() => { + try { + localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(sidebarCollapsed)) + } catch { + // Ignore storage errors + } + }, [sidebarCollapsed]) + useKeyboardShortcut(SHORTCUTS.COMMAND_PALETTE, () => { setCommandPaletteOpen(true) }) useKeyboardShortcut(SHORTCUTS.TOGGLE_SIDEBAR, () => { - setSidebarCollapsed(prev => !prev) + setSidebarCollapsed((prev: boolean) => !prev) }) return ( -
- {/* Top Navigation */} +
setSidebarCollapsed(!sidebarCollapsed)} sidebarCollapsed={sidebarCollapsed} @@ -33,13 +51,11 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { />
- {/* Sidebar */} setSidebarCollapsed(!sidebarCollapsed)} /> - {/* Main Content */}
- {/* Command Palette */} setCommandPaletteOpen(false)} />
) diff --git a/frontend/src/components/dashboard/DashboardStats.tsx b/frontend/src/components/dashboard/DashboardStats.tsx index 7d31fb1..6623ba1 100644 --- a/frontend/src/components/dashboard/DashboardStats.tsx +++ b/frontend/src/components/dashboard/DashboardStats.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' import { motion } from 'framer-motion' +import { FolderGit2, CheckCircle, Code2 } from 'lucide-react' import type { Repository } from '../../types' interface DashboardStatsProps { @@ -28,55 +29,66 @@ const useAnimatedCounter = (end: number, duration: number = 1200) => { export function DashboardStats({ repos }: DashboardStatsProps) { const totalRepos = repos.length const indexedRepos = repos.filter(r => r.status === 'indexed').length - const totalFunctions = repos.reduce((acc, r) => acc + (r.file_count || 0), 0) + const totalFiles = repos.reduce((acc, r) => acc + (r.file_count || 0), 0) const indexingCount = repos.filter(r => r.status === 'indexing' || r.status === 'cloning').length const animatedTotal = useAnimatedCounter(totalRepos) const animatedIndexed = useAnimatedCounter(indexedRepos) - const animatedFunctions = useAnimatedCounter(totalFunctions) + const animatedFiles = useAnimatedCounter(totalFiles) + + const stats = [ + { + label: 'Total Repositories', + value: animatedTotal, + icon: FolderGit2, + suffix: '', + }, + { + label: 'Indexed', + value: animatedIndexed, + icon: CheckCircle, + suffix: `/${totalRepos}`, + extra: indexingCount > 0 ? ( +
+ + {indexingCount} indexing... +
+ ) : null, + }, + { + label: 'Files Indexed', + value: animatedFiles, + icon: Code2, + suffix: '', + format: true, + }, + ] return (
- {/* Total Repos */} - -

Total Repositories

-

{animatedTotal}

-
- - {/* Indexed */} - -

Indexed

-
- {animatedIndexed} - /{totalRepos} -
- {indexingCount > 0 && ( -
- - {indexingCount} indexing... + {stats.map((stat, index) => ( + +
+

{stat.label}

+
- )} -
- - {/* Functions */} - -

Functions Indexed

-

{animatedFunctions.toLocaleString()}

-
+
+ + {stat.format ? stat.value.toLocaleString() : stat.value} + + {stat.suffix && ( + {stat.suffix} + )} +
+ {stat.extra} + + ))}
) } diff --git a/frontend/src/components/dashboard/Sidebar.tsx b/frontend/src/components/dashboard/Sidebar.tsx index 98a9d02..154c48e 100644 --- a/frontend/src/components/dashboard/Sidebar.tsx +++ b/frontend/src/components/dashboard/Sidebar.tsx @@ -1,48 +1,19 @@ import { Link, useLocation } from 'react-router-dom' -import { useAuth } from '../../contexts/AuthContext' +import { + FolderGit2, + Search, + Settings, + BookOpen, + ChevronLeft, + ChevronRight, + ExternalLink +} from 'lucide-react' interface SidebarProps { collapsed: boolean onToggle: () => void } -// Icons -const RepoIcon = () => ( - - - -) - -const SearchIcon = () => ( - - - -) - -const SettingsIcon = () => ( - - - - -) - -const ChevronIcon = ({ direction = 'left' }: { direction?: 'left' | 'right' }) => ( - - - -) - -const DocsIcon = () => ( - - - -) - interface NavItem { name: string href: string @@ -51,18 +22,17 @@ interface NavItem { } const mainNavItems: NavItem[] = [ - { name: 'Repositories', href: '/dashboard', icon: }, - { name: 'Global Search', href: '/dashboard/search', icon: }, + { name: 'Repositories', href: '/dashboard', icon: }, + { name: 'Global Search', href: '/dashboard/search', icon: }, ] const bottomNavItems: NavItem[] = [ - { name: 'Documentation', href: '/docs', icon: , external: true }, - { name: 'Settings', href: '/dashboard/settings', icon: }, + { name: 'Documentation', href: '/docs', icon: , external: true }, + { name: 'Settings', href: '/dashboard/settings', icon: }, ] export function Sidebar({ collapsed, onToggle }: SidebarProps) { const location = useLocation() - const { session } = useAuth() const isActive = (href: string) => { if (href === '/dashboard') { @@ -72,24 +42,31 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) { } const NavLink = ({ item }: { item: NavItem }) => { - // External links open in new tab + const active = isActive(item.href) + + const baseClasses = ` + flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all group + ${active + ? 'bg-primary/10 text-primary' + : 'text-muted-foreground hover:text-foreground hover:bg-muted' + } + ` + if (item.external) { return ( - + {item.icon} {!collapsed && ( <> {item.name} - - - + )} @@ -97,15 +74,8 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) { } return ( - - + + {item.icon} {!collapsed && ( @@ -117,30 +87,36 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) { return ( diff --git a/frontend/src/components/dashboard/TopNav.tsx b/frontend/src/components/dashboard/TopNav.tsx index 5aa596c..116ce1d 100644 --- a/frontend/src/components/dashboard/TopNav.tsx +++ b/frontend/src/components/dashboard/TopNav.tsx @@ -1,6 +1,19 @@ import { Link } from 'react-router-dom' import { useAuth } from '../../contexts/AuthContext' import { useState } from 'react' +import { + Menu, + Search, + Github, + Sun, + Moon, + LogOut, + Settings, + BookOpen, + ExternalLink +} from 'lucide-react' +import { useTheme } from 'next-themes' +import { Button } from '@/components/ui/button' interface TopNavProps { onToggleSidebar: () => void @@ -8,131 +21,128 @@ interface TopNavProps { onOpenCommandPalette?: () => void } -// Icons -const MenuIcon = () => ( - - - -) - -const SearchIcon = () => ( - - - -) - -const GitHubIcon = () => ( - - - -) - -const CodeIntelLogo = () => ( -
- CI -
-) - export function TopNav({ onToggleSidebar, sidebarCollapsed, onOpenCommandPalette }: TopNavProps) { const { session, signOut } = useAuth() + const { theme, setTheme } = useTheme() const [showUserMenu, setShowUserMenu] = useState(false) const userEmail = session?.user?.email || 'User' const userInitial = userEmail.charAt(0).toUpperCase() + const toggleTheme = () => { + setTheme(theme === 'dark' ? 'light' : 'dark') + } + return ( -