import { IoDocumentTextOutline } from 'react-icons/io5'
import { SearchMode } from './SearchInput'
import { SearchOptions } from './SearchOptions'
// Types from existing components
interface TextSearchResult {
id: string
name: string
file_extension: string
matches: {
line_number: number
content: string
context_before?: string
context_after?: string
}[]
total_matches: number
}
interface SemanticSearchResult {
chunk_id: string
file_id: string
file_name: string
content: string
similarity: number
line_start: number
line_end: number
}
interface SearchResultsProps {
mode: SearchMode
searchQuery: string
textResults: TextSearchResult[]
semanticResults: SemanticSearchResult[]
isSearching: boolean
searchOptions: SearchOptions
onResultClick: (result: any, lineNumber?: number) => void
}
export default function SearchResults({
mode,
searchQuery,
textResults,
semanticResults,
isSearching,
searchOptions,
onResultClick
}: SearchResultsProps) {
const highlightTextMatch = (text: string, query: string, caseSensitive: boolean = false) => {
if (!query) return text
try {
let pattern = query
if (!searchOptions.useRegex) {
pattern = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
const flags = caseSensitive ? 'g' : 'gi'
const regex = new RegExp(`(${pattern})`, flags)
return text.split(regex).map((part, index) => {
const testRegex = new RegExp(pattern, flags)
return testRegex.test(part) ? (
<span key={index} className="bg-blue-500/30 text-blue-200 rounded-sm px-1 py-0.5 font-semibold">
{part}
</span>
) : part
})
} catch (error) {
return text
}
}
const highlightSemanticMatch = (text: string, query: string) => {
if (!query) return text
try {
const words = query.toLowerCase().split(/\s+/)
let highlightedText = text
words.forEach(word => {
if (word.length > 2) {
const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
highlightedText = highlightedText.replace(regex, (match) =>
`<mark class="bg-violet-400/25 text-violet-200 rounded-sm px-1 py-0.5 font-semibold">${match}</mark>`
)
}
})
return <span dangerouslySetInnerHTML={{ __html: highlightedText }} />
} catch (error) {
return text
}
}
const totalTextResults = textResults.reduce((sum, file) => sum + file.total_matches, 0)
if (isSearching) {
return (
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center">
<div className={`animate-spin rounded-full h-6 w-6 border-3 border-transparent mx-auto mb-3 shadow-lg ${
mode === 'semantic'
? 'border-t-violet-500 border-r-purple-500'
: 'border-t-blue-500 border-r-cyan-500'
}`} />
<p className="text-xs font-semibold text-slate-300 mb-1">
{mode === 'semantic' ? 'AI searching...' : 'Searching...'}
</p>
<p className="text-xs text-slate-500">
{mode === 'semantic' ? 'Finding by meaning' : 'Scanning for matches'}
</p>
</div>
</div>
)
}
if (!searchQuery) {
return (
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center max-w-48">
<div className={`text-3xl mb-3 ${
mode === 'semantic' ? 'text-violet-400/60' : 'text-slate-500'
}`}>
{mode === 'semantic' ? 'โจ' : '๐'}
</div>
<p className="text-xs font-semibold text-slate-300 mb-1.5">
{mode === 'semantic' ? 'AI Search' : 'Text Search'}
</p>
<p className="text-xs text-slate-500 leading-tight">
{mode === 'semantic'
? 'Find by meaning & context'
: 'Exact text matching'
}
</p>
</div>
</div>
)
}
// Text Search Results
if (mode === 'text') {
if (textResults.length === 0) {
return (
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center">
<div className="text-3xl mb-3 text-slate-500">๐</div>
<p className="text-xs font-semibold text-slate-300 mb-1">No results found</p>
<p className="text-xs text-slate-500">Try different keywords</p>
</div>
</div>
)
}
return (
<div className="flex-1 overflow-y-auto">
<div className="p-2 space-y-1.5">
{textResults.map((result) => (
<div key={result.id} className="group bg-slate-900/40 backdrop-blur-sm rounded-lg border border-slate-800/50 hover:border-slate-700/60 transition-all duration-200 hover:shadow-lg hover:bg-slate-900/60">
<div
className="flex items-center gap-2.5 text-sm text-slate-300 cursor-pointer hover:text-slate-100 p-3 group/header"
onClick={() => onResultClick(result)}
>
<div className="p-1.5 bg-blue-500/10 rounded-md border border-blue-500/20 group-hover/header:bg-blue-500/20 transition-colors">
<IoDocumentTextOutline className="w-3.5 h-3.5 text-blue-400" />
</div>
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold text-blue-300 bg-blue-500/10 px-2 py-0.5 rounded border border-blue-500/20">
{result.total_matches}
</span>
</div>
</div>
<div className="px-3 pb-3 space-y-1">
{result.matches.slice(0, 3).map((match, index) => (
<div
key={index}
className="group/match cursor-pointer hover:bg-slate-800/40 p-2 rounded-md transition-all duration-150 border border-transparent hover:border-slate-700/50"
onClick={() => onResultClick(result, match.line_number)}
>
<div className="flex items-start gap-2">
<span className="text-slate-500 text-xs font-mono bg-slate-800/50 px-1.5 py-0.5 rounded text-center min-w-[2.5rem]">
{match.line_number}
</span>
<span className="text-slate-300 flex-1 font-mono text-xs leading-tight group-hover/match:text-slate-200 transition-colors">
{highlightTextMatch(match.content.trim(), searchQuery, searchOptions.matchCase)}
</span>
</div>
</div>
))}
{result.matches.length > 3 && (
<div className="text-xs text-slate-500 px-2 py-1.5 bg-slate-800/30 rounded border border-slate-800/50 text-center">
+{result.matches.length - 3} more
</div>
)}
</div>
</div>
))}
</div>
{/* Results Summary */}
<div className="px-3 py-2 border-t border-slate-800/60 backdrop-blur-sm">
<div className="text-xs font-semibold text-slate-400 text-center">
{totalTextResults} result{totalTextResults !== 1 ? 's' : ''} in {textResults.length} file{textResults.length !== 1 ? 's' : ''}
</div>
</div>
</div>
)
}
// Semantic Search Results
if (semanticResults.length === 0) {
return (
<div className="flex-1 flex items-center justify-center p-6">
<div className="text-center">
<div className="text-3xl mb-3 text-slate-500">๐</div>
<p className="text-xs font-semibold text-slate-300 mb-1">No matches found</p>
<p className="text-xs text-slate-500">Try rephrasing</p>
</div>
</div>
)
}
const getSimilarityColor = (similarity: number) => {
if (similarity >= 0.8) return 'text-emerald-400'
if (similarity >= 0.6) return 'text-yellow-400'
if (similarity >= 0.4) return 'text-orange-400'
return 'text-red-400'
}
const getSimilarityBadgeColor = (similarity: number) => {
if (similarity >= 0.8) return 'bg-emerald-500/10 border-emerald-500/20 text-emerald-300'
if (similarity >= 0.6) return 'bg-yellow-500/10 border-yellow-500/20 text-yellow-300'
if (similarity >= 0.4) return 'bg-orange-500/10 border-orange-500/20 text-orange-300'
return 'bg-red-500/10 border-red-500/20 text-red-300'
}
const getSimilarityLabel = (similarity: number) => {
if (similarity >= 0.8) return 'Great'
if (similarity >= 0.6) return 'Good'
if (similarity >= 0.4) return 'OK'
return 'Fair'
}
return (
<div className="flex-1 overflow-y-auto">
<div className="p-2 space-y-1.5">
{semanticResults.map((result) => (
<div
key={result.chunk_id}
className="group cursor-pointer bg-slate-900/40 backdrop-blur-sm rounded-lg border border-slate-800/50 hover:border-violet-500/30 transition-all duration-200 hover:shadow-lg hover:bg-slate-900/60"
onClick={() => onResultClick(result)}
>
<div className="p-3">
<div className="flex items-center gap-2.5 mb-2.5">
<div className="p-1.5 bg-violet-500/10 rounded-md border border-violet-500/20 group-hover:bg-violet-500/20 transition-colors">
<IoDocumentTextOutline className="w-3.5 h-3.5 text-violet-400" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-0.5">
<span className="text-xs font-semibold text-slate-200 truncate group-hover:text-slate-100 transition-colors">
{result.file_name}
</span>
</div>
<div className="text-xs text-slate-500 font-medium">
L{result.line_start}โ{result.line_end}
</div>
</div>
<div className="flex items-center gap-1.5">
<span className={`text-xs font-bold ${getSimilarityColor(result.similarity)}`}>
{getSimilarityLabel(result.similarity)}
</span>
<span className={`text-xs font-semibold px-2 py-0.5 rounded border ${getSimilarityBadgeColor(result.similarity)}`}>
{Math.round(result.similarity * 100)}%
</span>
</div>
</div>
<div className="relative p-2.5 bg-slate-900/60 backdrop-blur-sm rounded-md border border-violet-500/20 text-xs text-slate-300 font-mono leading-tight overflow-hidden">
<div className="absolute top-0 left-0 w-0.5 h-full bg-gradient-to-b from-violet-500 to-purple-500"></div>
<div className="pl-2.5">
{highlightSemanticMatch(result.content.trim(), searchQuery)}
</div>
</div>
</div>
</div>
))}
</div>
{/* Results Summary */}
<div className="px-3 py-2 border-t border-slate-800/60 backdrop-blur-sm">
<div className="text-xs font-semibold text-slate-400 text-center">
{semanticResults.length} result{semanticResults.length !== 1 ? 's' : ''}
</div>
</div>
</div>
)
}