diff --git a/docker-compose.yml b/docker-compose.yml index 392a515..4b40e61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,7 @@ services: environment: - REDIS_HOST=redis - REDIS_PORT=6379 + - ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173,http://localhost:5174 - OPENAI_API_KEY=${OPENAI_API_KEY} - PINECONE_API_KEY=${PINECONE_API_KEY} - PINECONE_INDEX_NAME=${PINECONE_INDEX_NAME} diff --git a/frontend/src/components/SearchPanel.tsx b/frontend/src/components/SearchPanel.tsx index 66aaeb6..5ea22b8 100644 --- a/frontend/src/components/SearchPanel.tsx +++ b/frontend/src/components/SearchPanel.tsx @@ -1,100 +1,84 @@ -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 type { SearchResult } from '../types' +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 type { SearchResult } from '../types'; interface SearchPanelProps { - repoId: string - apiUrl: string - apiKey: string + repoId: string; + apiUrl: string; + apiKey: string; } export function SearchPanel({ repoId, apiUrl, apiKey }: 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 [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 handleSearch = async (e: React.FormEvent) => { - e.preventDefault() - if (!query.trim()) return + const handleSearch = async () => { + if (!query.trim()) return; - setLoading(true) - const startTime = Date.now() + setLoading(true); + setHasSearched(true); + const startTime = Date.now(); try { const response = await fetch(`${apiUrl}/search`, { method: 'POST', headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', }, body: JSON.stringify({ query, repo_id: repoId, - max_results: 10 - }) - }) + max_results: 10, + }), + }); - const data = await response.json() - setResults(data.results || []) - setSearchTime(Date.now() - startTime) - setCached(data.cached || false) + const data = await response.json(); + setResults(data.results || []); + setSearchTime(Date.now() - startTime); + setCached(data.cached || false); } catch (error) { - console.error('Search error:', error) + console.error('Search error:', error); toast.error('Search failed', { - description: 'Please check your query and try again' - }) + description: 'Please check your query and try again', + }); } finally { - setLoading(false) + setLoading(false); } - } + }; return (
- {/* Search */} -
-
-
- setQuery(e.target.value)} - placeholder="e.g., authentication middleware, React hooks, database queries..." - className="flex-1 px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-gray-500 focus:outline-none focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/20 transition-all" - disabled={loading} - autoFocus - /> - -
-

- Powered by semantic embeddings - finds code by meaning, not just keywords -

-
+ {/* Search Box */} +
+ {searchTime !== null && ( -
+
- {results.length} results + {results.length} results - + - {searchTime}ms + {searchTime}ms {cached && ( <> - - - ⚡ Cached - + + ⚡ Cached )}
@@ -104,37 +88,40 @@ 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
-
+
Match
+
{(result.score * 100).toFixed(0)}%
- {/* Code with Syntax Highlighting */} + {/* Code */}
{result.code} - +
- + {result.language}
- {/* Metadata */} -
+ {/* Footer */} +
Lines {result.line_start}–{result.line_end} - - - {result.file_path} - + + {result.file_path}
))}
{/* Empty State */} - {results.length === 0 && query && !loading && ( -
-
+ {results.length === 0 && hasSearched && !loading && ( +
+
🔍
-

No results found

-

+

No results found

+

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

)}
- ) + ); } diff --git a/frontend/src/components/search/SearchBox.tsx b/frontend/src/components/search/SearchBox.tsx new file mode 100644 index 0000000..d3616e3 --- /dev/null +++ b/frontend/src/components/search/SearchBox.tsx @@ -0,0 +1,172 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; +import { Search, Command, Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface SearchBoxProps { + value: string; + onChange: (value: string) => void; + onSubmit: () => void; + placeholder?: string; + loading?: boolean; + disabled?: boolean; + autoFocus?: boolean; + className?: string; +} + +const PLACEHOLDER_EXAMPLES = [ + 'Search for authentication logic...', + 'Find API endpoints...', + 'Look up database queries...', + 'Discover React hooks...', + 'Find error handling patterns...', +]; + +export function SearchBox({ + value, + onChange, + onSubmit, + placeholder, + loading = false, + disabled = false, + autoFocus = false, + className, +}: SearchBoxProps) { + const inputRef = useRef(null); + const [isFocused, setIsFocused] = useState(false); + const [placeholderIndex, setPlaceholderIndex] = useState(0); + const [animatedPlaceholder, setAnimatedPlaceholder] = useState(''); + const [isTyping, setIsTyping] = useState(true); + + // Keyboard shortcut: ⌘K or Ctrl+K to focus + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + inputRef.current?.focus(); + } + if (e.key === 'Escape' && document.activeElement === inputRef.current) { + inputRef.current?.blur(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, []); + + // Animated placeholder typewriter effect + useEffect(() => { + if (placeholder || value) return; + + const currentText = PLACEHOLDER_EXAMPLES[placeholderIndex]; + let charIndex = 0; + let timeout: NodeJS.Timeout; + + if (isTyping) { + const typeChar = () => { + if (charIndex <= currentText.length) { + setAnimatedPlaceholder(currentText.slice(0, charIndex)); + charIndex++; + timeout = setTimeout(typeChar, 50); + } else { + timeout = setTimeout(() => setIsTyping(false), 2000); + } + }; + typeChar(); + } else { + charIndex = currentText.length; + const deleteChar = () => { + if (charIndex >= 0) { + setAnimatedPlaceholder(currentText.slice(0, charIndex)); + charIndex--; + timeout = setTimeout(deleteChar, 30); + } else { + setPlaceholderIndex((prev) => (prev + 1) % PLACEHOLDER_EXAMPLES.length); + setIsTyping(true); + } + }; + deleteChar(); + } + + return () => clearTimeout(timeout); + }, [placeholderIndex, isTyping, placeholder, value]); + + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!value.trim() || loading || disabled) return; + onSubmit(); + }, + [value, loading, disabled, onSubmit] + ); + + const displayPlaceholder = placeholder || animatedPlaceholder || PLACEHOLDER_EXAMPLES[0]; + + return ( +
+
+ {/* Search icon */} +
+ {loading ? ( + + ) : ( + + )} +
+ + {/* Input */} + onChange(e.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + placeholder={displayPlaceholder} + disabled={disabled || loading} + autoFocus={autoFocus} + className="flex-1 bg-transparent text-text-primary text-base placeholder:text-text-muted focus:outline-none disabled:cursor-not-allowed" + /> + + {/* Keyboard shortcut hint */} + {!isFocused && !value && ( +
+ + + + + K + +
+ )} + + {/* Submit button */} + {value && ( + + )} +
+ +

+ Semantic search — finds code by meaning, not just keywords +

+
+ ); +} diff --git a/frontend/src/components/search/index.ts b/frontend/src/components/search/index.ts new file mode 100644 index 0000000..0ba5871 --- /dev/null +++ b/frontend/src/components/search/index.ts @@ -0,0 +1 @@ +export { SearchBox } from './SearchBox';