diff --git a/frontend/src/components/DependencyGraph.tsx b/frontend/src/components/DependencyGraph.tsx index 2b60f9f..01d426c 100644 --- a/frontend/src/components/DependencyGraph.tsx +++ b/frontend/src/components/DependencyGraph.tsx @@ -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 @@ -174,12 +175,7 @@ export function DependencyGraph({ repoId, apiUrl, apiKey }: DependencyGraphProps } if (loading) { - return ( -
-
-

Building dependency graph...

-
- ) + return } return ( diff --git a/frontend/src/components/SearchPanel.tsx b/frontend/src/components/SearchPanel.tsx index 998d962..9ef4395 100644 --- a/frontend/src/components/SearchPanel.tsx +++ b/frontend/src/components/SearchPanel.tsx @@ -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 { @@ -21,8 +23,8 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }: const [hasSearched, setHasSearched] = useState(false); const [aiSummary, setAiSummary] = useState(null); - const handleSearch = async () => { - if (!query.trim()) return; + const searchWithQuery = async (searchQuery: string) => { + if (!searchQuery.trim()) return; setLoading(true); setHasSearched(true); setAiSummary(null); @@ -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 || []); @@ -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 (
{/* Search Box */} @@ -70,30 +79,34 @@ export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl, defaultBranch }: )}
+ {/* Loading Skeleton */} + {loading && ( + + )} + {/* Results */} -
- {results.map((result, idx) => ( - - ))} -
+ {!loading && ( +
+ {results.map((result, idx) => ( + + ))} +
+ )} {/* Empty State */} - {results.length === 0 && hasSearched && !loading && ( -
-
- -
-

No results found

-

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

-
+ {results.length === 0 && hasSearched && !loading && query && ( + )}
); diff --git a/frontend/src/components/StyleInsights.tsx b/frontend/src/components/StyleInsights.tsx index 99ebb45..0535be1 100644 --- a/frontend/src/components/StyleInsights.tsx +++ b/frontend/src/components/StyleInsights.tsx @@ -1,4 +1,5 @@ import { useStyleAnalysis } from '../hooks/useCachedQuery' +import { StyleInsightsSkeleton } from './ui/Skeleton' interface StyleInsightsProps { repoId: string @@ -10,12 +11,7 @@ export function StyleInsights({ repoId, apiUrl, apiKey }: StyleInsightsProps) { const { data, isLoading: loading } = useStyleAnalysis({ repoId, apiKey }) if (loading) { - return ( -
-
-

Analyzing code style patterns...

-
- ) + return } if (!data) return null diff --git a/frontend/src/components/search/SearchEmptyState.tsx b/frontend/src/components/search/SearchEmptyState.tsx new file mode 100644 index 0000000..cf777c8 --- /dev/null +++ b/frontend/src/components/search/SearchEmptyState.tsx @@ -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 ( +
+ {/* Header */} +
+
+ +
+

No results for "{query}"

+

+ We couldn't find any code matching your query. Try one of the suggestions below or refine your search. +

+
+ +
+ {/* Query Suggestions */} + {suggestions.length > 0 && ( +
+

+ + Try these instead +

+
+ {suggestions.map((suggestion, idx) => ( + + ))} +
+
+ )} + + {/* Example Queries */} +
+

+ + Popular searches +

+
+ {EXAMPLE_QUERIES.map((example, idx) => ( + + ))} +
+
+
+ + {/* Search Tips */} +
+

+ + Search Tips +

+
    + {SEARCH_TIPS.map((tip, idx) => ( +
  • + + {tip} +
  • + ))} +
+
+
+ ) +} diff --git a/frontend/src/components/ui/Skeleton.tsx b/frontend/src/components/ui/Skeleton.tsx index aababfc..d005ac7 100644 --- a/frontend/src/components/ui/Skeleton.tsx +++ b/frontend/src/components/ui/Skeleton.tsx @@ -8,7 +8,7 @@ export function Skeleton({ className }: SkeletonProps) { return (
@@ -18,7 +18,7 @@ export function Skeleton({ className }: SkeletonProps) { // Preset skeleton components export function RepoCardSkeleton() { return ( -
+
@@ -49,7 +49,7 @@ export function RepoGridSkeleton({ count = 3 }: { count?: number }) { export function StatCardSkeleton() { return ( -
+
@@ -58,16 +58,175 @@ export function StatCardSkeleton() { export function SearchResultSkeleton() { return ( -
-
-
- +
+ {/* Header */} +
+
+
+ + +
- +
+ + +
+
+ {/* Code block */} +
+
+ + + + + +
+
+ {/* Footer */} +
+ +
+ + +
+
+
+ ) +} + +export function SearchResultsSkeleton({ count = 3 }: { count?: number }) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( + + ))} +
+ ) +} + +export function DependencyGraphSkeleton() { + return ( +
+ {/* Metrics Grid */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+ + {/* Graph Area */} +
+
+ +
+ + +
+
+ {/* Fake graph visualization */} +
+
+
+ {/* Center node */} + + {/* Surrounding nodes */} + {[0, 60, 120, 180, 240, 300].map((angle, i) => ( + + ))} +
+
+
+ +
+
+
+ + {/* Critical Files */} +
+ +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+
+
+ ) +} + +export function StyleInsightsSkeleton() { + return ( +
+ {/* Summary Cards */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+ + {/* Two Column Layout */} +
+ {/* Naming Conventions */} +
+ +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+ + +
+ +
+ ))} +
+
+ + {/* Common Patterns */} +
+ +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+
+
+ + {/* Language Distribution */} +
+ +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+
- -
) } @@ -76,7 +235,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) { return (
{Array.from({ length: rows }).map((_, i) => ( -
+