Skip to content

Commit b672728

Browse files
authored
Merge pull request #219 from DevanshuNEU/feature/dashboard-v2-polish
feat(dashboard): Phase 2.5b/c — Loading Skeletons & Search Empty State
2 parents 41cecf9 + 8c44166 commit b672728

5 files changed

Lines changed: 346 additions & 48 deletions

File tree

frontend/src/components/DependencyGraph.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import dagre from 'dagre'
1212
import { Lightbulb } from 'lucide-react'
1313
import 'reactflow/dist/style.css'
1414
import { useDependencyGraph } from '../hooks/useCachedQuery'
15+
import { DependencyGraphSkeleton } from './ui/Skeleton'
1516

1617
interface DependencyGraphProps {
1718
repoId: string
@@ -174,12 +175,7 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps
174175
}
175176

176177
if (loading) {
177-
return (
178-
<div className="p-12 text-center">
179-
<div className="w-16 h-16 border-4 border-primary/20 border-t-primary rounded-full animate-spin mx-auto mb-4" />
180-
<p className="text-muted-foreground">Building dependency graph...</p>
181-
</div>
182-
)
178+
return <DependencyGraphSkeleton />
183179
}
184180

185181
return (

frontend/src/components/SearchPanel.tsx

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useState } from 'react';
22
import { toast } from 'sonner';
3-
import { Zap, Search } from 'lucide-react';
3+
import { Zap } from 'lucide-react';
44
import { SearchBox, ResultCard } from './search';
5+
import { SearchEmptyState } from './search/SearchEmptyState';
6+
import { SearchResultsSkeleton } from './ui/Skeleton';
57
import type { SearchResult } from '../types';
68

79
interface SearchPanelProps {
@@ -21,8 +23,8 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
2123
const [hasSearched, setHasSearched] = useState(false);
2224
const [aiSummary, setAiSummary] = useState<string | null>(null);
2325

24-
const handleSearch = async () => {
25-
if (!query.trim()) return;
26+
const searchWithQuery = async (searchQuery: string) => {
27+
if (!searchQuery.trim()) return;
2628
setLoading(true);
2729
setHasSearched(true);
2830
setAiSummary(null);
@@ -32,7 +34,7 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
3234
const response = await fetch(`${apiUrl}/search`, {
3335
method: 'POST',
3436
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
35-
body: JSON.stringify({ query, repo_id: repoId, max_results: 10 }),
37+
body: JSON.stringify({ query: searchQuery, repo_id: repoId, max_results: 10 }),
3638
});
3739
const data = await response.json();
3840
setResults(data.results || []);
@@ -47,6 +49,13 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
4749
}
4850
};
4951

52+
const handleSearch = () => searchWithQuery(query);
53+
54+
const handleSuggestionClick = (suggestion: string) => {
55+
setQuery(suggestion);
56+
searchWithQuery(suggestion);
57+
};
58+
5059
return (
5160
<div className="p-6 space-y-6">
5261
{/* Search Box */}
@@ -70,30 +79,34 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
7079
)}
7180
</div>
7281

82+
{/* Loading Skeleton */}
83+
{loading && (
84+
<SearchResultsSkeleton count={3} />
85+
)}
86+
7387
{/* Results */}
74-
<div className="space-y-3">
75-
{results.map((result, idx) => (
76-
<ResultCard
77-
key={`${result.file_path}-${result.line_start}-${idx}`}
78-
result={result}
79-
rank={idx + 1}
80-
isExpanded={idx === 0}
81-
aiSummary={idx === 0 ? aiSummary || undefined : undefined}
82-
repoUrl={repoUrl}
83-
defaultBranch={defaultBranch}
84-
/>
85-
))}
86-
</div>
88+
{!loading && (
89+
<div className="space-y-3">
90+
{results.map((result, idx) => (
91+
<ResultCard
92+
key={`${result.file_path}-${result.line_start}-${idx}`}
93+
result={result}
94+
rank={idx + 1}
95+
isExpanded={idx === 0}
96+
aiSummary={idx === 0 ? aiSummary || undefined : undefined}
97+
repoUrl={repoUrl}
98+
defaultBranch={defaultBranch}
99+
/>
100+
))}
101+
</div>
102+
)}
87103

88104
{/* Empty State */}
89-
{results.length === 0 && hasSearched && !loading && (
90-
<div className="bg-muted border border-border rounded-xl p-16 text-center">
91-
<div className="w-20 h-20 mx-auto mb-4 rounded-2xl bg-background border border-border flex items-center justify-center">
92-
<Search className="w-10 h-10 text-muted-foreground" />
93-
</div>
94-
<h3 className="text-base font-semibold mb-2 text-foreground">No results found</h3>
95-
<p className="text-sm text-muted-foreground">Try a different query or check if the repository is fully indexed</p>
96-
</div>
105+
{results.length === 0 && hasSearched && !loading && query && (
106+
<SearchEmptyState
107+
query={query}
108+
onSuggestionClick={handleSuggestionClick}
109+
/>
97110
)}
98111
</div>
99112
);

frontend/src/components/StyleInsights.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useStyleAnalysis } from '../hooks/useCachedQuery'
2+
import { StyleInsightsSkeleton } from './ui/Skeleton'
23

34
interface StyleInsightsProps {
45
repoId: string
@@ -10,12 +11,7 @@ export function StyleInsights({ repoId, apiUrl, apiKey }: StyleInsightsProps) {
1011
const { data, isLoading: loading } = useStyleAnalysis({ repoId, apiKey })
1112

1213
if (loading) {
13-
return (
14-
<div className="p-12 text-center">
15-
<div className="w-16 h-16 border-4 border-primary/20 border-t-primary rounded-full animate-spin mx-auto mb-4" />
16-
<p className="text-muted-foreground">Analyzing code style patterns...</p>
17-
</div>
18-
)
14+
return <StyleInsightsSkeleton />
1915
}
2016

2117
if (!data) return null
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { Search, Lightbulb, ArrowRight, Code2, FileSearch, Sparkles } from 'lucide-react'
2+
import { Button } from '@/components/ui/button'
3+
4+
interface SearchEmptyStateProps {
5+
query: string
6+
onSuggestionClick: (suggestion: string) => void
7+
}
8+
9+
const EXAMPLE_QUERIES = [
10+
{ query: 'authentication', description: 'Find auth logic' },
11+
{ query: 'error handling', description: 'Locate error handlers' },
12+
{ query: 'api routes', description: 'Discover API endpoints' },
13+
{ query: 'database', description: 'Find DB operations' },
14+
]
15+
16+
const SEARCH_TIPS = [
17+
'Use natural language: "function that handles user login"',
18+
'Search by concept: "error handling" or "validation"',
19+
'Find patterns: "async function" or "useEffect hook"',
20+
'Be specific: "parse JSON response" beats "parse"',
21+
]
22+
23+
export function SearchEmptyState({ query, onSuggestionClick }: SearchEmptyStateProps) {
24+
// Generate query-specific suggestions
25+
const getSuggestions = (q: string): string[] => {
26+
const lowerQ = q.toLowerCase()
27+
const suggestions: string[] = []
28+
29+
// If query is very short, suggest expanding
30+
if (q.length < 4) {
31+
suggestions.push(`${q} function`, `${q} handler`, `${q} component`)
32+
}
33+
34+
// Common refinements
35+
if (lowerQ.includes('auth')) {
36+
suggestions.push('login handler', 'authentication middleware', 'session management')
37+
} else if (lowerQ.includes('api') || lowerQ.includes('route')) {
38+
suggestions.push('REST endpoint', 'API handler', 'route middleware')
39+
} else if (lowerQ.includes('error')) {
40+
suggestions.push('error boundary', 'try catch block', 'error middleware')
41+
} else if (lowerQ.includes('test')) {
42+
suggestions.push('unit test', 'test helper', 'mock function')
43+
} else {
44+
// Generic suggestions based on query
45+
suggestions.push(
46+
`${q} implementation`,
47+
`${q} helper function`,
48+
`how to ${q}`
49+
)
50+
}
51+
52+
return suggestions.slice(0, 3)
53+
}
54+
55+
const suggestions = query ? getSuggestions(query) : []
56+
57+
return (
58+
<div className="bg-card border border-border rounded-xl overflow-hidden">
59+
{/* Header */}
60+
<div className="p-8 text-center border-b border-border bg-muted/30">
61+
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center">
62+
<FileSearch className="w-8 h-8 text-primary" />
63+
</div>
64+
<h3 className="text-lg font-semibold mb-2 text-foreground">No results for "{query}"</h3>
65+
<p className="text-sm text-muted-foreground max-w-md mx-auto">
66+
We couldn't find any code matching your query. Try one of the suggestions below or refine your search.
67+
</p>
68+
</div>
69+
70+
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
71+
{/* Query Suggestions */}
72+
{suggestions.length > 0 && (
73+
<div>
74+
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
75+
<Sparkles className="w-4 h-4 text-primary" />
76+
Try these instead
77+
</h4>
78+
<div className="space-y-2">
79+
{suggestions.map((suggestion, idx) => (
80+
<button
81+
key={idx}
82+
onClick={() => onSuggestionClick(suggestion)}
83+
className="w-full flex items-center justify-between p-3 bg-muted/50 hover:bg-muted rounded-lg text-left transition-colors group"
84+
>
85+
<span className="text-sm text-foreground">{suggestion}</span>
86+
<ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" />
87+
</button>
88+
))}
89+
</div>
90+
</div>
91+
)}
92+
93+
{/* Example Queries */}
94+
<div>
95+
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
96+
<Code2 className="w-4 h-4 text-primary" />
97+
Popular searches
98+
</h4>
99+
<div className="space-y-2">
100+
{EXAMPLE_QUERIES.map((example, idx) => (
101+
<button
102+
key={idx}
103+
onClick={() => onSuggestionClick(example.query)}
104+
className="w-full flex items-center justify-between p-3 bg-muted/50 hover:bg-muted rounded-lg text-left transition-colors group"
105+
>
106+
<div>
107+
<span className="text-sm text-foreground">{example.query}</span>
108+
<span className="text-xs text-muted-foreground ml-2">{example.description}</span>
109+
</div>
110+
<ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" />
111+
</button>
112+
))}
113+
</div>
114+
</div>
115+
</div>
116+
117+
{/* Search Tips */}
118+
<div className="p-5 bg-muted/30 border-t border-border">
119+
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3 flex items-center gap-2">
120+
<Lightbulb className="w-3.5 h-3.5" />
121+
Search Tips
122+
</h4>
123+
<ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
124+
{SEARCH_TIPS.map((tip, idx) => (
125+
<li key={idx} className="text-xs text-muted-foreground flex items-start gap-2">
126+
<span className="text-primary"></span>
127+
{tip}
128+
</li>
129+
))}
130+
</ul>
131+
</div>
132+
</div>
133+
)
134+
}

0 commit comments

Comments
 (0)