diff --git a/frontend/components/knowledge-filter-panel.tsx b/frontend/components/knowledge-filter-panel.tsx index 9a850f73..6aedff22 100644 --- a/frontend/components/knowledge-filter-panel.tsx +++ b/frontend/components/knowledge-filter-panel.tsx @@ -62,12 +62,12 @@ export function KnowledgeFilterPanel() { // Set the actual filter selections from the saved knowledge filter const filters = parsedFilterData.filters - // If arrays are empty, default to wildcard (match everything) - // Otherwise use the specific selections from the saved filter + // Use the exact selections from the saved filter + // Empty arrays mean "none selected" not "all selected" const processedFilters = { - data_sources: filters.data_sources.length === 0 ? ["*"] : filters.data_sources, - document_types: filters.document_types.length === 0 ? ["*"] : filters.document_types, - owners: filters.owners.length === 0 ? ["*"] : filters.owners + data_sources: filters.data_sources, + document_types: filters.document_types, + owners: filters.owners } console.log("[DEBUG] Loading filter selections:", processedFilters) @@ -258,7 +258,8 @@ export function KnowledgeFilterPanel() { }) => { if (!buckets || buckets.length === 0) return null - const isAllSelected = selectedFilters[facetType].includes("*") // Wildcard + // "All" is selected if it contains wildcard OR if no specific selections are made + const isAllSelected = selectedFilters[facetType].includes("*") const handleAllToggle = (checked: boolean) => { if (checked) { diff --git a/frontend/src/app/search/page.tsx b/frontend/src/app/search/page.tsx index 865944e1..0c93a734 100644 --- a/frontend/src/app/search/page.tsx +++ b/frontend/src/app/search/page.tsx @@ -6,10 +6,13 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { Search, Loader2, FileText, Zap } from "lucide-react" +import { Badge } from "@/components/ui/badge" +import { Search, Loader2, FileText, Zap, RefreshCw } from "lucide-react" import { ProtectedRoute } from "@/components/protected-route" import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context" +type FacetBucket = { key: string; count: number } + interface SearchResult { filename: string mimetype: string @@ -34,6 +37,12 @@ function SearchPage() { const [searchPerformed, setSearchPerformed] = useState(false) const prevFilterDataRef = useRef("") + // Stats state for knowledge overview + const [statsLoading, setStatsLoading] = useState(false) + const [totalDocs, setTotalDocs] = useState(0) + const [totalChunks, setTotalChunks] = useState(0) + const [facetStats, setFacetStats] = useState<{ data_sources: FacetBucket[]; document_types: FacetBucket[]; owners: FacetBucket[] } | null>(null) + const handleSearch = useCallback(async (e?: React.FormEvent) => { if (e) e.preventDefault() if (!query.trim()) return @@ -151,172 +160,206 @@ function SearchPage() { prevFilterDataRef.current = currentFilterString }, [parsedFilterData, searchPerformed, query, handleSearch]) + // Fetch stats with current knowledge filter applied + const fetchStats = async () => { + try { + setStatsLoading(true) + + // Build search payload with current filter data + const searchPayload: any = { + query: '*', + limit: 0, + scoreThreshold: parsedFilterData?.scoreThreshold || 0 + } + + // Add filters from global context if available and not wildcards + if (parsedFilterData?.filters) { + const filters = parsedFilterData.filters + + // Only include filters if they're not wildcards (not "*") + const hasSpecificFilters = + !filters.data_sources.includes("*") || + !filters.document_types.includes("*") || + !filters.owners.includes("*") + + if (hasSpecificFilters) { + const processedFilters: any = {} + + // Only add filter arrays that don't contain wildcards + if (!filters.data_sources.includes("*")) { + processedFilters.data_sources = filters.data_sources + } + if (!filters.document_types.includes("*")) { + processedFilters.document_types = filters.document_types + } + if (!filters.owners.includes("*")) { + processedFilters.owners = filters.owners + } + + // Only add filters object if it has any actual filters + if (Object.keys(processedFilters).length > 0) { + searchPayload.filters = processedFilters + } + } + } + + const response = await fetch('/api/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(searchPayload) + }) + const result = await response.json() + if (response.ok) { + const aggs = result.aggregations || {} + const toBuckets = (agg: { buckets?: Array<{ key: string | number; doc_count: number }> }): FacetBucket[] => + (agg?.buckets || []).map(b => ({ key: String(b.key), count: b.doc_count })) + const dataSourceBuckets = toBuckets(aggs.data_sources) + setFacetStats({ + data_sources: dataSourceBuckets.slice(0, 10), + document_types: toBuckets(aggs.document_types).slice(0, 10), + owners: toBuckets(aggs.owners).slice(0, 10) + }) + setTotalDocs(dataSourceBuckets.length) + setTotalChunks(Number(result.total || 0)) + } + } catch { + // non-fatal – keep page functional without stats + } finally { + setStatsLoading(false) + } + } + + // Initial stats fetch and refresh when filter changes + useEffect(() => { + fetchStats() + }, [parsedFilterData]) + return ( -
- {/* Hero Section */} -
-
-

- Search -

-
-

- Find documents using hybrid search -

-

- Enter your search query to find relevant documents using AI-powered semantic search combined with keyword matching across your document collection. -

-
- - {/* Search Interface */} - - - - - Search Documents - {selectedFilter && ( - - Filter: {selectedFilter.name} - - )} - - - Enter your search query to find relevant documents using hybrid search (semantic + keyword) - {selectedFilter && ( - - Using knowledge filter: {selectedFilter.name} - - )} - - - -
-
- -
- setQuery(e.target.value)} - className="h-12 bg-background/50 border-border/50 focus:border-blue-400/50 focus:ring-blue-400/20 flex-1" - /> - -
-
+
+
+ {/* Search Input Area */} +
+ + setQuery(e.target.value)} + className="flex-1 bg-muted/20 rounded-lg border border-border/50 px-4 py-3 h-12 focus-visible:ring-1 focus-visible:ring-ring" + /> + +
- {/* Search Results */} - {searchPerformed && ( + {/* Results Area */} +
+ {searchPerformed ? (
- {/* Search Results Header */} -
-

- - Search Results -

-
-
- - {results.length} result{results.length !== 1 ? 's' : ''} returned - + {results.length === 0 ? ( +
+ +

No documents found

+

+ Try adjusting your search terms +

-
- - {/* Results */} -
- {results.length === 0 ? ( - - -
-
- -
-

- No documents found -

-

- Try adjusting your search terms or modify your knowledge filter settings. -

-
-
-
- ) : ( + ) : ( + <> +
+ {results.length} result{results.length !== 1 ? 's' : ''} found +
{results.map((result, index) => ( - - -
- -
- -
- {result.filename} -
-
-
- - {result.score.toFixed(2)} - -
-
+
+
+
+ + {result.filename}
- - - {result.mimetype} - - - Page {result.page} - - - - -
-

- {result.text} -

-
-
- + + {result.score.toFixed(2)} + +
+
+ {result.mimetype} • Page {result.page} +
+

+ {result.text} +

+
))}
- )} + + )} +
+ ) : ( + /* Knowledge Overview - Show when no search has been performed */ +
+
+
+

Knowledge Overview

+
+ + {/* Documents row */} +
+
Total documents
+
{statsLoading ? '—' : totalDocs}
+
+ + {/* Separator */} +
+ + {/* Chunks and breakdown */} +
+
+
Total chunks
+
{statsLoading ? '—' : totalChunks}
+
+
+
Top types
+
+ {(facetStats?.document_types || []).slice(0,5).map((b) => ( + {b.key} · {b.count} + ))} +
+
+
+
Top owners
+
+ {(facetStats?.owners || []).slice(0,5).map((b) => ( + {b.key || 'unknown'} · {b.count} + ))} +
+
+
+
Top files
+
+ {(facetStats?.data_sources || []).slice(0,5).map((b) => ( + {b.key} · {b.count} + ))} +
+
+
)} - - {/* Empty State */} - {!searchPerformed && ( -
-

- Enter a search query above to get started -

-
- )} - - +
+
) }