Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions frontend/src/components/DependencyGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import dagre from 'dagre'
import { Lightbulb } from 'lucide-react'
import 'reactflow/dist/style.css'
import { useDependencyGraph } from '../hooks/useCachedQuery'
import { DependencyGraphSkeleton } from './ui/Skeleton'

interface DependencyGraphProps {
repoId: string
Expand Down Expand Up @@ -174,12 +175,7 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps
}

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

return (
Expand Down
63 changes: 38 additions & 25 deletions frontend/src/components/SearchPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { Zap, Search } from 'lucide-react';
import { Zap } from 'lucide-react';
import { SearchBox, ResultCard } from './search';
import { SearchEmptyState } from './search/SearchEmptyState';
import { SearchResultsSkeleton } from './ui/Skeleton';
import type { SearchResult } from '../types';

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

const handleSearch = async () => {
if (!query.trim()) return;
const searchWithQuery = async (searchQuery: string) => {
if (!searchQuery.trim()) return;
setLoading(true);
setHasSearched(true);
setAiSummary(null);
Expand All @@ -32,7 +34,7 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
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 }),
body: JSON.stringify({ query: searchQuery, repo_id: repoId, max_results: 10 }),
});
const data = await response.json();
setResults(data.results || []);
Expand All @@ -47,6 +49,13 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
}
};

const handleSearch = () => searchWithQuery(query);

const handleSuggestionClick = (suggestion: string) => {
setQuery(suggestion);
searchWithQuery(suggestion);
};

return (
<div className="p-6 space-y-6">
{/* Search Box */}
Expand All @@ -70,30 +79,34 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }:
)}
</div>

{/* Loading Skeleton */}
{loading && (
<SearchResultsSkeleton count={3} />
)}

{/* Results */}
<div className="space-y-3">
{results.map((result, idx) => (
<ResultCard
key={`${result.file_path}-${result.line_start}-${idx}`}
result={result}
rank={idx + 1}
isExpanded={idx === 0}
aiSummary={idx === 0 ? aiSummary || undefined : undefined}
repoUrl={repoUrl}
defaultBranch={defaultBranch}
/>
))}
</div>
{!loading && (
<div className="space-y-3">
{results.map((result, idx) => (
<ResultCard
key={`${result.file_path}-${result.line_start}-${idx}`}
result={result}
rank={idx + 1}
isExpanded={idx === 0}
aiSummary={idx === 0 ? aiSummary || undefined : undefined}
repoUrl={repoUrl}
defaultBranch={defaultBranch}
/>
))}
</div>
)}

{/* Empty State */}
{results.length === 0 && hasSearched && !loading && (
<div className="bg-muted border border-border rounded-xl p-16 text-center">
<div className="w-20 h-20 mx-auto mb-4 rounded-2xl bg-background border border-border flex items-center justify-center">
<Search className="w-10 h-10 text-muted-foreground" />
</div>
<h3 className="text-base font-semibold mb-2 text-foreground">No results found</h3>
<p className="text-sm text-muted-foreground">Try a different query or check if the repository is fully indexed</p>
</div>
{results.length === 0 && hasSearched && !loading && query && (
<SearchEmptyState
query={query}
onSuggestionClick={handleSuggestionClick}
/>
)}
</div>
);
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/components/StyleInsights.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useStyleAnalysis } from '../hooks/useCachedQuery'
import { StyleInsightsSkeleton } from './ui/Skeleton'

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

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

if (!data) return null
Expand Down
134 changes: 134 additions & 0 deletions frontend/src/components/search/SearchEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Search, Lightbulb, ArrowRight, Code2, FileSearch, Sparkles } from 'lucide-react'
import { Button } from '@/components/ui/button'

interface SearchEmptyStateProps {
query: string
onSuggestionClick: (suggestion: string) => void
}

const EXAMPLE_QUERIES = [
{ query: 'authentication', description: 'Find auth logic' },
{ query: 'error handling', description: 'Locate error handlers' },
{ query: 'api routes', description: 'Discover API endpoints' },
{ query: 'database', description: 'Find DB operations' },
]

const SEARCH_TIPS = [
'Use natural language: "function that handles user login"',
'Search by concept: "error handling" or "validation"',
'Find patterns: "async function" or "useEffect hook"',
'Be specific: "parse JSON response" beats "parse"',
]

export function SearchEmptyState({ query, onSuggestionClick }: SearchEmptyStateProps) {
// Generate query-specific suggestions
const getSuggestions = (q: string): string[] => {
const lowerQ = q.toLowerCase()
const suggestions: string[] = []

// If query is very short, suggest expanding
if (q.length < 4) {
suggestions.push(`${q} function`, `${q} handler`, `${q} component`)
}

// Common refinements
if (lowerQ.includes('auth')) {
suggestions.push('login handler', 'authentication middleware', 'session management')
} else if (lowerQ.includes('api') || lowerQ.includes('route')) {
suggestions.push('REST endpoint', 'API handler', 'route middleware')
} else if (lowerQ.includes('error')) {
suggestions.push('error boundary', 'try catch block', 'error middleware')
} else if (lowerQ.includes('test')) {
suggestions.push('unit test', 'test helper', 'mock function')
} else {
// Generic suggestions based on query
suggestions.push(
`${q} implementation`,
`${q} helper function`,
`how to ${q}`
)
}

return suggestions.slice(0, 3)
}

const suggestions = query ? getSuggestions(query) : []

return (
<div className="bg-card border border-border rounded-xl overflow-hidden">
{/* Header */}
<div className="p-8 text-center border-b border-border bg-muted/30">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center">
<FileSearch className="w-8 h-8 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2 text-foreground">No results for "{query}"</h3>
<p className="text-sm text-muted-foreground max-w-md mx-auto">
We couldn't find any code matching your query. Try one of the suggestions below or refine your search.
</p>
</div>

<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Query Suggestions */}
{suggestions.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-primary" />
Try these instead
</h4>
<div className="space-y-2">
{suggestions.map((suggestion, idx) => (
<button
key={idx}
onClick={() => onSuggestionClick(suggestion)}
className="w-full flex items-center justify-between p-3 bg-muted/50 hover:bg-muted rounded-lg text-left transition-colors group"
>
<span className="text-sm text-foreground">{suggestion}</span>
<ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" />
</button>
))}
</div>
</div>
)}

{/* Example Queries */}
<div>
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<Code2 className="w-4 h-4 text-primary" />
Popular searches
</h4>
<div className="space-y-2">
{EXAMPLE_QUERIES.map((example, idx) => (
<button
key={idx}
onClick={() => onSuggestionClick(example.query)}
className="w-full flex items-center justify-between p-3 bg-muted/50 hover:bg-muted rounded-lg text-left transition-colors group"
>
<div>
<span className="text-sm text-foreground">{example.query}</span>
<span className="text-xs text-muted-foreground ml-2">β€” {example.description}</span>
</div>
<ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" />
</button>
))}
</div>
</div>
</div>

{/* Search Tips */}
<div className="p-5 bg-muted/30 border-t border-border">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3 flex items-center gap-2">
<Lightbulb className="w-3.5 h-3.5" />
Search Tips
</h4>
<ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
{SEARCH_TIPS.map((tip, idx) => (
<li key={idx} className="text-xs text-muted-foreground flex items-start gap-2">
<span className="text-primary">β€’</span>
{tip}
</li>
))}
</ul>
</div>
</div>
)
}
Loading