|
import { useState } from "react"; |
|
import { Button } from "@/components/ui/button"; |
|
import { SortAsc, Filter, Circle } from "lucide-react"; |
|
import ResultCard from "./result-card"; |
|
import { type SearchResponse, type DocumentWithContext } from "@shared/schema"; |
|
|
|
interface SearchResultsProps { |
|
results?: SearchResponse; |
|
expandedResults: Set<number>; |
|
savedDocuments?: Set<number>; |
|
onToggleExpanded: (resultId: number) => void; |
|
onAddCitation: (documentId: number, citationText: string, section?: string, pageNumber?: number) => void; |
|
onSaveDocument?: (documentId: number) => void; |
|
isLoading?: boolean; |
|
error?: Error | null; |
|
} |
|
|
|
export default function SearchResults({ |
|
results, |
|
expandedResults, |
|
savedDocuments, |
|
onToggleExpanded, |
|
onAddCitation, |
|
onSaveDocument, |
|
isLoading, |
|
error |
|
}: SearchResultsProps) { |
|
const [sortBy, setSortBy] = useState<"relevance" | "date" | "title">("relevance"); |
|
|
|
if (error) { |
|
return null; |
|
} |
|
|
|
if (isLoading) { |
|
return ( |
|
<div className="space-y-4"> |
|
{[...Array(3)].map((_, i) => ( |
|
<div key={i} className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 animate-pulse"> |
|
<div className="flex items-start justify-between mb-4"> |
|
<div className="flex-1 space-y-2"> |
|
<div className="flex items-center gap-3 mb-2"> |
|
<div className="w-4 h-4 bg-slate-200 rounded"></div> |
|
<div className="w-20 h-4 bg-slate-200 rounded"></div> |
|
<div className="w-16 h-6 bg-slate-200 rounded-full"></div> |
|
</div> |
|
<div className="w-3/4 h-6 bg-slate-200 rounded"></div> |
|
<div className="w-1/2 h-4 bg-slate-200 rounded"></div> |
|
</div> |
|
<div className="w-8 h-8 bg-slate-200 rounded"></div> |
|
</div> |
|
<div className="bg-slate-50 rounded-lg p-4"> |
|
<div className="space-y-2"> |
|
<div className="w-full h-4 bg-slate-200 rounded"></div> |
|
<div className="w-5/6 h-4 bg-slate-200 rounded"></div> |
|
<div className="w-4/6 h-4 bg-slate-200 rounded"></div> |
|
</div> |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
); |
|
} |
|
|
|
const sortedResults = results?.results ? [...results.results].sort((a, b) => { |
|
switch (sortBy) { |
|
case "relevance": |
|
return b.relevanceScore - a.relevanceScore; |
|
case "date": |
|
return new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime(); |
|
case "title": |
|
return a.title.localeCompare(b.title); |
|
default: |
|
return 0; |
|
} |
|
}) : []; |
|
|
|
if (!results) { |
|
return null; |
|
} |
|
|
|
return ( |
|
<div> |
|
{/* Results Statistics */} |
|
<div className="flex items-center justify-between mb-6"> |
|
<div className="flex items-center gap-4"> |
|
<span className="text-sm text-slate-600"> |
|
<span className="font-medium">{results?.totalCount || 0}</span> results found in{" "} |
|
<span className="font-medium">{results?.searchTime?.toFixed(2) || '0.00'}</span> seconds |
|
</span> |
|
<div className="flex items-center gap-2"> |
|
<Circle className="w-2 h-2 fill-emerald-500 text-emerald-500" /> |
|
<span className="text-sm text-slate-600">Vector index active</span> |
|
</div> |
|
</div> |
|
<div className="flex items-center gap-2"> |
|
<Button |
|
variant="ghost" |
|
size="sm" |
|
onClick={() => { |
|
const nextSort = sortBy === "relevance" ? "date" : sortBy === "date" ? "title" : "relevance"; |
|
setSortBy(nextSort); |
|
}} |
|
className="text-slate-400 hover:text-slate-600" |
|
> |
|
<SortAsc className="w-4 h-4" /> |
|
</Button> |
|
<Button |
|
variant="ghost" |
|
size="sm" |
|
className="text-slate-400 hover:text-slate-600" |
|
> |
|
<Filter className="w-4 h-4" /> |
|
</Button> |
|
</div> |
|
</div> |
|
|
|
{/* Search Results */} |
|
<div className="space-y-4"> |
|
{sortedResults.map((result) => ( |
|
<ResultCard |
|
key={result.id} |
|
document={result} |
|
isExpanded={expandedResults.has(result.id)} |
|
isSaved={savedDocuments?.has(result.id) || false} |
|
onToggleExpanded={() => onToggleExpanded(result.id)} |
|
onAddCitation={onAddCitation} |
|
onSaveDocument={onSaveDocument} |
|
/> |
|
))} |
|
</div> |
|
|
|
{/* Load More */} |
|
{results?.results && results.results.length > 0 && results.totalCount > results.results.length && ( |
|
<div className="mt-8 text-center"> |
|
<Button |
|
variant="outline" |
|
className="px-6 py-3" |
|
> |
|
Load More Results |
|
</Button> |
|
</div> |
|
)} |
|
</div> |
|
); |
|
} |
|
|