Skip to content

Commit 8a6e6e3

Browse files
committed
feat(overview): add AI Summary Hero Card
- Created RepoSummaryCard component with auto-generated insights - Fetches data from /insights and /style-analysis endpoints - Shows: main summary, quick stats pills, critical files warning - Added useRepoInsights hook with caching support - Card only shows when repo status is 'indexed' - Gradient background with primary color theming - Loading skeleton while data fetches Phase 2.5a complete - the 'aha moment' is here!
1 parent ffc5025 commit 8a6e6e3

3 files changed

Lines changed: 186 additions & 0 deletions

File tree

frontend/src/components/RepoOverview.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion'
33
import { toast } from 'sonner'
44
import { Progress } from '@/components/ui/progress'
55
import { Button } from '@/components/ui/button'
6+
import { RepoSummaryCard } from './RepoSummaryCard'
67
import type { Repository } from '../types'
78
import { WS_URL } from '../config/api'
89
import { useInvalidateRepoCache } from '../hooks/useCachedQuery'
@@ -84,6 +85,11 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr
8485

8586
return (
8687
<div className="p-6 space-y-6">
88+
{/* AI Summary Card - Hero Section */}
89+
{repo.status === 'indexed' && (
90+
<RepoSummaryCard repo={repo} apiKey={apiKey} />
91+
)}
92+
8793
{/* Stats Grid */}
8894
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
8995
{/* Status */}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { motion } from 'framer-motion'
2+
import { Sparkles, GitBranch, Code2, FileCode, AlertTriangle } from 'lucide-react'
3+
import { useRepoInsights, useStyleAnalysis } from '../hooks/useCachedQuery'
4+
import type { Repository } from '../types'
5+
6+
interface RepoSummaryCardProps {
7+
repo: Repository
8+
apiKey: string
9+
}
10+
11+
export function RepoSummaryCard({ repo, apiKey }: RepoSummaryCardProps) {
12+
const { data: insights, isLoading: insightsLoading } = useRepoInsights({ repoId: repo.id, apiKey })
13+
const { data: style, isLoading: styleLoading } = useStyleAnalysis({ repoId: repo.id, apiKey })
14+
15+
const isLoading = insightsLoading || styleLoading
16+
17+
if (isLoading) {
18+
return (
19+
<motion.div
20+
initial={{ opacity: 0, y: 12 }}
21+
animate={{ opacity: 1, y: 0 }}
22+
className="bg-gradient-to-br from-primary/5 via-primary/10 to-violet-500/5 border border-primary/20 rounded-xl p-6"
23+
>
24+
<div className="flex items-start gap-4">
25+
<div className="w-10 h-10 rounded-lg bg-primary/20 animate-pulse" />
26+
<div className="flex-1 space-y-3">
27+
<div className="h-5 w-48 bg-primary/20 rounded animate-pulse" />
28+
<div className="h-4 w-full bg-primary/10 rounded animate-pulse" />
29+
<div className="h-4 w-3/4 bg-primary/10 rounded animate-pulse" />
30+
</div>
31+
</div>
32+
</motion.div>
33+
)
34+
}
35+
36+
// Generate summary from insights + style data
37+
const summary = generateSummary(repo, insights, style)
38+
39+
return (
40+
<motion.div
41+
initial={{ opacity: 0, y: 12 }}
42+
animate={{ opacity: 1, y: 0 }}
43+
className="bg-gradient-to-br from-primary/5 via-primary/10 to-violet-500/5 border border-primary/20 rounded-xl p-6"
44+
>
45+
<div className="flex items-start gap-4">
46+
<div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center shrink-0">
47+
<Sparkles className="w-5 h-5 text-primary" />
48+
</div>
49+
50+
<div className="flex-1 min-w-0">
51+
<h3 className="text-sm font-semibold text-primary mb-2 flex items-center gap-2">
52+
AI Summary
53+
<span className="text-[10px] px-1.5 py-0.5 bg-primary/20 text-primary rounded font-normal">Auto-generated</span>
54+
</h3>
55+
56+
<p className="text-foreground leading-relaxed mb-4">{summary.main}</p>
57+
58+
{/* Quick Stats Pills */}
59+
<div className="flex flex-wrap gap-2">
60+
{summary.stats.map((stat, idx) => (
61+
<div
62+
key={idx}
63+
className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-background border border-border rounded-lg text-xs"
64+
>
65+
<stat.icon className="w-3.5 h-3.5 text-muted-foreground" />
66+
<span className="text-muted-foreground">{stat.label}:</span>
67+
<span className="font-semibold text-foreground">{stat.value}</span>
68+
</div>
69+
))}
70+
</div>
71+
72+
{/* Critical Files Warning */}
73+
{summary.criticalFiles && summary.criticalFiles.length > 0 && (
74+
<div className="mt-4 pt-4 border-t border-border">
75+
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-2">
76+
<AlertTriangle className="w-3.5 h-3.5 text-yellow-500" />
77+
<span>High-impact files (most dependencies)</span>
78+
</div>
79+
<div className="flex flex-wrap gap-1.5">
80+
{summary.criticalFiles.slice(0, 3).map((file, idx) => (
81+
<code key={idx} className="text-xs px-2 py-1 bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border border-yellow-500/20 rounded">
82+
{file}
83+
</code>
84+
))}
85+
</div>
86+
</div>
87+
)}
88+
</div>
89+
</div>
90+
</motion.div>
91+
)
92+
}
93+
94+
function generateSummary(repo: Repository, insights: any, style: any) {
95+
const stats: { icon: any; label: string; value: string }[] = []
96+
const criticalFiles: string[] = []
97+
98+
// Extract key metrics
99+
const fileCount = insights?.total_files || repo.file_count || 0
100+
const functionCount = style?.summary?.total_functions || insights?.total_functions || 0
101+
const languages = insights?.languages || style?.language_distribution || {}
102+
const primaryLang = Object.keys(languages)[0] || 'Unknown'
103+
const asyncAdoption = style?.summary?.async_adoption || null
104+
const typeHints = style?.summary?.type_hints_usage || null
105+
106+
// Get naming convention
107+
const namingConventions = style?.naming_conventions?.functions || {}
108+
const primaryNaming = Object.entries(namingConventions)
109+
.sort((a: any, b: any) => parseFloat(b[1].percentage) - parseFloat(a[1].percentage))[0]
110+
const namingStyle = primaryNaming ? primaryNaming[0] : null
111+
112+
// Get critical files
113+
if (insights?.most_critical_files) {
114+
insights.most_critical_files.slice(0, 3).forEach((f: any) => {
115+
criticalFiles.push(f.file.split('/').pop() || f.file)
116+
})
117+
}
118+
119+
// Build stats array
120+
stats.push({ icon: FileCode, label: 'Files', value: fileCount.toLocaleString() })
121+
stats.push({ icon: Code2, label: 'Functions', value: functionCount.toLocaleString() })
122+
stats.push({ icon: GitBranch, label: 'Branch', value: repo.branch })
123+
124+
if (asyncAdoption && asyncAdoption !== '0%') {
125+
stats.push({ icon: Code2, label: 'Async', value: asyncAdoption })
126+
}
127+
128+
// Generate main summary text
129+
let main = `**${repo.name}** is a `
130+
131+
if (functionCount > 500) main += 'large '
132+
else if (functionCount > 100) main += 'medium-sized '
133+
else main += 'compact '
134+
135+
main += `${primaryLang} codebase with ${functionCount.toLocaleString()} functions across ${fileCount} files. `
136+
137+
if (namingStyle) {
138+
main += `The code primarily uses ${namingStyle} naming conventions`
139+
if (primaryNaming && parseFloat((primaryNaming[1] as any).percentage) > 80) {
140+
main += ` (${(primaryNaming[1] as any).percentage} consistency)`
141+
}
142+
main += '. '
143+
}
144+
145+
if (typeHints && typeHints !== '0%' && parseFloat(typeHints) > 50) {
146+
main += `Strong type coverage at ${typeHints}. `
147+
}
148+
149+
if (criticalFiles.length > 0) {
150+
main += `Core architecture centers around ${criticalFiles[0]}.`
151+
}
152+
153+
return { main, stats, criticalFiles }
154+
}

frontend/src/hooks/useCachedQuery.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,31 @@ export function useStyleAnalysis({ repoId, apiKey, enabled = true }: UseCachedQu
9494
})
9595
}
9696

97+
/**
98+
* Hook for fetching repository insights with caching
99+
*/
100+
export function useRepoInsights({ repoId, apiKey, enabled = true }: UseCachedQueryOptions) {
101+
return useQuery({
102+
queryKey: ['insights', repoId],
103+
queryFn: async () => {
104+
const data = await fetchWithAuth(
105+
`${API_URL}/repos/${repoId}/insights`,
106+
apiKey
107+
)
108+
saveToCache('insights', repoId, data)
109+
return data
110+
},
111+
enabled: enabled && !!repoId && !!apiKey,
112+
staleTime: STALE_TIME,
113+
gcTime: CACHE_TIME,
114+
initialData: () => getFromCache('insights', repoId),
115+
initialDataUpdatedAt: () => {
116+
const cached = getFromCache('insights', repoId)
117+
return cached ? Date.now() - STALE_TIME + 1000 : 0
118+
}
119+
})
120+
}
121+
97122
/**
98123
* Hook for fetching impact analysis with caching
99124
*/
@@ -134,6 +159,7 @@ export function useInvalidateRepoCache() {
134159
queryClient.invalidateQueries({ queryKey: ['dependencies', repoId] })
135160
queryClient.invalidateQueries({ queryKey: ['style-analysis', repoId] })
136161
queryClient.invalidateQueries({ queryKey: ['impact', repoId] })
162+
queryClient.invalidateQueries({ queryKey: ['insights', repoId] })
137163

138164
// Invalidate localStorage cache
139165
invalidateRepoCache(repoId)

0 commit comments

Comments
 (0)