diff --git a/frontend/src/components/SearchPanel.tsx b/frontend/src/components/SearchPanel.tsx index 5ea22b8..4d0a2d7 100644 --- a/frontend/src/components/SearchPanel.tsx +++ b/frontend/src/components/SearchPanel.tsx @@ -1,29 +1,30 @@ import { useState } from 'react'; import { toast } from 'sonner'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { SearchBox } from './search'; +import { SearchBox, ResultCard } from './search'; import type { SearchResult } from '../types'; interface SearchPanelProps { repoId: string; apiUrl: string; apiKey: string; + repoUrl?: string; } -export function SearchPanel({ repoId, apiUrl, apiKey }: SearchPanelProps) { +export function SearchPanel({ repoId, apiUrl, apiKey, repoUrl }: SearchPanelProps) { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [searchTime, setSearchTime] = useState(null); const [cached, setCached] = useState(false); const [hasSearched, setHasSearched] = useState(false); + const [aiSummary, setAiSummary] = useState(null); const handleSearch = async () => { if (!query.trim()) return; setLoading(true); setHasSearched(true); + setAiSummary(null); const startTime = Date.now(); try { @@ -44,6 +45,10 @@ export function SearchPanel({ repoId, apiUrl, apiKey }: SearchPanelProps) { setResults(data.results || []); setSearchTime(Date.now() - startTime); setCached(data.cached || false); + + if (data.ai_summary) { + setAiSummary(data.ai_summary); + } } catch (error) { console.error('Search error:', error); toast.error('Search failed', { @@ -72,8 +77,8 @@ export function SearchPanel({ repoId, apiUrl, apiKey }: SearchPanelProps) { {results.length} results - - {searchTime}ms + + {searchTime}ms {cached && ( <> @@ -86,83 +91,16 @@ export function SearchPanel({ repoId, apiUrl, apiKey }: SearchPanelProps) { {/* Results */} -
+
{results.map((result, idx) => ( -
- {/* Header */} -
-
-
-

- {result.name} -

- - {result.type.replace('_', ' ')} - -
-

- {result.file_path.split('/').slice(-3).join('/')} -

-
- -
-
-
Match
-
- {(result.score * 100).toFixed(0)}% -
-
- -
-
- - {/* Code */} -
- - {result.code} - - -
- - {result.language} - -
-
- - {/* Footer */} -
- - Lines {result.line_start}–{result.line_end} - - - {result.file_path} -
-
+ ))}
diff --git a/frontend/src/components/dashboard/DashboardHome.tsx b/frontend/src/components/dashboard/DashboardHome.tsx index 045e856..d96e7da 100644 --- a/frontend/src/components/dashboard/DashboardHome.tsx +++ b/frontend/src/components/dashboard/DashboardHome.tsx @@ -216,7 +216,8 @@ export function DashboardHome() { )} diff --git a/frontend/src/components/search/ResultCard.tsx b/frontend/src/components/search/ResultCard.tsx new file mode 100644 index 0000000..35079b5 --- /dev/null +++ b/frontend/src/components/search/ResultCard.tsx @@ -0,0 +1,194 @@ +import { useState, useRef, useEffect } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { toast } from 'sonner'; +import type { SearchResult } from '../../types'; + +interface ResultCardProps { + result: SearchResult; + rank: number; + isExpanded?: boolean; + aiSummary?: string; + repoUrl?: string; +} + +export function ResultCard({ + result, + rank, + isExpanded: initialExpanded = false, + aiSummary, + repoUrl +}: ResultCardProps) { + const [expanded, setExpanded] = useState(initialExpanded); + const contentRef = useRef(null); + const [contentHeight, setContentHeight] = useState( + initialExpanded ? undefined : 0 + ); + + const matchPercent = Math.round(result.score * 100); + const isTopResult = rank === 1; + + // Extract clean file path (remove repos/{uuid}/ prefix if present) + const cleanFilePath = result.file_path.replace(/^repos\/[a-f0-9-]+\//, ''); + const displayPath = cleanFilePath.split('/').slice(-3).join('/'); + + // Build GitHub URL with clean path + const githubUrl = repoUrl + ? `${repoUrl}/blob/main/${cleanFilePath}#L${result.line_start}-L${result.line_end}` + : null; + + // Animate height on expand/collapse + useEffect(() => { + if (expanded) { + const height = contentRef.current?.scrollHeight; + setContentHeight(height); + // After animation, set to auto for dynamic content + const timer = setTimeout(() => setContentHeight(undefined), 200); + return () => clearTimeout(timer); + } else { + // First set explicit height, then animate to 0 + const height = contentRef.current?.scrollHeight; + setContentHeight(height); + requestAnimationFrame(() => setContentHeight(0)); + } + }, [expanded]); + + const copyCode = () => { + navigator.clipboard.writeText(result.code); + toast.success('Copied to clipboard'); + }; + + return ( +
+ {/* Header */} + + + {/* Expandable content with animation */} +
+
+ {/* AI Summary */} + {aiSummary && isTopResult && ( +
+
+ +
+

AI Summary

+

{aiSummary}

+
+
+
+ )} + + {/* Code block */} +
+ + {result.code} + + + + {result.language} + +
+ + {/* Footer */} +
+ + Lines {result.line_start}–{result.line_end} + + +
+ + + {githubUrl && ( + + + + + View + + )} +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/search/index.ts b/frontend/src/components/search/index.ts index 0ba5871..a902070 100644 --- a/frontend/src/components/search/index.ts +++ b/frontend/src/components/search/index.ts @@ -1 +1,2 @@ export { SearchBox } from './SearchBox'; +export { ResultCard } from './ResultCard';