Skip to content

Commit 8c44166

Browse files
committed
feat(search): smart empty state with suggestions
- Add SearchEmptyState component with: - Query-specific suggestions (refines based on keywords) - Popular example queries - Search tips section - Click suggestion to auto-search - Refactor SearchPanel to use searchWithQuery function
1 parent 84eccfe commit 8c44166

2 files changed

Lines changed: 151 additions & 12 deletions

File tree

frontend/src/components/SearchPanel.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
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';
56
import { SearchResultsSkeleton } from './ui/Skeleton';
67
import type { SearchResult } from '../types';
78

@@ -22,8 +23,8 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
2223
const [hasSearched, setHasSearched] = useState(false);
2324
const [aiSummary, setAiSummary] = useState<string | null>(null);
2425

25-
const handleSearch = async () => {
26-
if (!query.trim()) return;
26+
const searchWithQuery = async (searchQuery: string) => {
27+
if (!searchQuery.trim()) return;
2728
setLoading(true);
2829
setHasSearched(true);
2930
setAiSummary(null);
@@ -33,7 +34,7 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
3334
const response = await fetch(`${apiUrl}/search`, {
3435
method: 'POST',
3536
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
36-
body: JSON.stringify({ query, repo_id: repoId, max_results: 10 }),
37+
body: JSON.stringify({ query: searchQuery, repo_id: repoId, max_results: 10 }),
3738
});
3839
const data = await response.json();
3940
setResults(data.results || []);
@@ -48,6 +49,13 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
4849
}
4950
};
5051

52+
const handleSearch = () => searchWithQuery(query);
53+
54+
const handleSuggestionClick = (suggestion: string) => {
55+
setQuery(suggestion);
56+
searchWithQuery(suggestion);
57+
};
58+
5159
return (
5260
<div className="p-6 space-y-6">
5361
{/* Search Box */}
@@ -94,14 +102,11 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
94102
)}
95103

96104
{/* Empty State */}
97-
{results.length === 0 && hasSearched && !loading && (
98-
<div className="bg-muted border border-border rounded-xl p-16 text-center">
99-
<div className="w-20 h-20 mx-auto mb-4 rounded-2xl bg-background border border-border flex items-center justify-center">
100-
<Search className="w-10 h-10 text-muted-foreground" />
101-
</div>
102-
<h3 className="text-base font-semibold mb-2 text-foreground">No results found</h3>
103-
<p className="text-sm text-muted-foreground">Try a different query or check if the repository is fully indexed</p>
104-
</div>
105+
{results.length === 0 && hasSearched && !loading && query && (
106+
<SearchEmptyState
107+
query={query}
108+
onSuggestionClick={handleSuggestionClick}
109+
/>
105110
)}
106111
</div>
107112
);
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)