"use client" import { useState, useEffect, useCallback, useRef } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Search, Loader2, FileText, HardDrive, Building2, Cloud } from "lucide-react" import { TbBrandOnedrive } from "react-icons/tb" import { SiGoogledrive } from "react-icons/si" import { ProtectedRoute } from "@/components/protected-route" import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context" import { useTask } from "@/contexts/task-context" import { KnowledgeDropdown } from "@/components/knowledge-dropdown" interface ChunkResult { filename: string mimetype: string page: number text: string score: number source_url?: string owner?: string owner_name?: string owner_email?: string file_size?: number connector_type?: string } interface FileResult { filename: string mimetype: string chunkCount: number avgScore: number source_url?: string owner?: string owner_name?: string owner_email?: string lastModified?: string size?: number connector_type?: string } interface SearchResponse { results: ChunkResult[] files?: FileResult[] error?: string total?: number aggregations?: { data_sources?: { buckets?: Array<{ key: string | number; doc_count: number }> } document_types?: { buckets?: Array<{ key: string | number; doc_count: number }> } owners?: { buckets?: Array<{ key: string | number; doc_count: number }> } connector_types?: { buckets?: Array<{ key: string | number; doc_count: number }> } } } // Function to get the appropriate icon for a connector type function getSourceIcon(connectorType?: string) { switch (connectorType) { case 'google_drive': return case 'onedrive': return case 'sharepoint': return case 's3': return case 'local': default: return } } function SearchPage() { const { isMenuOpen } = useTask() const { parsedFilterData, isPanelOpen } = useKnowledgeFilter() const [query, setQuery] = useState("*") const [loading, setLoading] = useState(false) const [chunkResults, setChunkResults] = useState([]) const [fileResults, setFileResults] = useState([]) const [selectedFile, setSelectedFile] = useState(null) const [searchPerformed, setSearchPerformed] = useState(false) const prevFilterDataRef = useRef("") const handleSearch = useCallback(async (e?: React.FormEvent) => { if (e) e.preventDefault() if (!query.trim()) return setLoading(true) setSearchPerformed(false) try { // Build search payload with global filter data interface SearchPayload { query: string; limit: number; scoreThreshold: number; filters?: { data_sources?: string[]; document_types?: string[]; owners?: string[]; connector_types?: string[]; }; } const searchPayload: SearchPayload = { query, limit: parsedFilterData?.limit || (query.trim() === "*" ? 10000 : 10), // Maximum allowed limit for wildcard searches scoreThreshold: parsedFilterData?.scoreThreshold || 0 } // Debug logging for wildcard searches if (query.trim() === "*") { console.log("Wildcard search - parsedFilterData:", parsedFilterData) } // 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("*") || (filters.connector_types && !filters.connector_types.includes("*")) if (hasSpecificFilters) { const processedFilters: SearchPayload['filters'] = {} // 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 } if (filters.connector_types && !filters.connector_types.includes("*")) { processedFilters.connector_types = filters.connector_types } // Only add filters object if it has any actual filters if (Object.keys(processedFilters).length > 0) { searchPayload.filters = processedFilters } } // If all filters are wildcards, omit the filters object entirely } const response = await fetch("/api/search", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(searchPayload), }) const result: SearchResponse = await response.json() if (response.ok) { const chunks = result.results || [] // Debug logging for wildcard searches if (query.trim() === "*") { console.log("Wildcard search results:", { chunks: chunks.length, totalFromBackend: result.total, searchPayload, firstChunk: chunks[0] }) } setChunkResults(chunks) // Group chunks by filename to create file results const fileMap = new Map() chunks.forEach(chunk => { const existing = fileMap.get(chunk.filename) if (existing) { existing.chunks.push(chunk) existing.totalScore += chunk.score } else { fileMap.set(chunk.filename, { filename: chunk.filename, mimetype: chunk.mimetype, chunks: [chunk], totalScore: chunk.score, source_url: chunk.source_url, owner: chunk.owner, owner_name: chunk.owner_name, owner_email: chunk.owner_email, file_size: chunk.file_size, connector_type: chunk.connector_type }) } }) const files: FileResult[] = Array.from(fileMap.values()).map(file => ({ filename: file.filename, mimetype: file.mimetype, chunkCount: file.chunks.length, avgScore: file.totalScore / file.chunks.length, source_url: file.source_url, owner: file.owner, owner_name: file.owner_name, owner_email: file.owner_email, size: file.file_size, connector_type: file.connector_type })) setFileResults(files) setSearchPerformed(true) } else { console.error("Search failed:", result.error) setChunkResults([]) setFileResults([]) setSearchPerformed(true) } } catch (error) { console.error("Search error:", error) setChunkResults([]) setFileResults([]) setSearchPerformed(true) } finally { setLoading(false) } }, [query, parsedFilterData]) // Update query when global filter changes useEffect(() => { if (parsedFilterData?.query) { setQuery(parsedFilterData.query) } }, [parsedFilterData]) // Auto-refresh search when filter changes (but only if search was already performed) useEffect(() => { if (!parsedFilterData) return // Create a stable string representation of the filter data for comparison const currentFilterString = JSON.stringify({ filters: parsedFilterData.filters, limit: parsedFilterData.limit, scoreThreshold: parsedFilterData.scoreThreshold }) // Only trigger search if filter data actually changed and we've done a search before if (prevFilterDataRef.current !== "" && prevFilterDataRef.current !== currentFilterString && searchPerformed && query.trim()) { console.log("Filter changed, auto-refreshing search") handleSearch() } // Update the ref with current filter data prevFilterDataRef.current = currentFilterString }, [parsedFilterData, searchPerformed, query, handleSearch]) // Auto-search on mount with "*" useEffect(() => { // Only trigger initial search on mount when query is "*" handleSearch() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Only run once on mount - ignore handleSearch dependency return (
{/* 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" />
{/* Results Area */}
{fileResults.length === 0 && chunkResults.length === 0 && !loading ? (

No documents found

Try adjusting your search terms

) : ( <> {/* Results Count */}
{fileResults.length} file{fileResults.length !== 1 ? 's' : ''} found
{/* Results Display */}
{selectedFile ? ( // Show chunks for selected file <>
Chunks from {selectedFile}
{chunkResults .filter(chunk => chunk.filename === selectedFile) .map((chunk, index) => (
{chunk.filename}
{chunk.score.toFixed(2)}
{chunk.mimetype} • Page {chunk.page}

{chunk.text}

))} ) : ( // Show files table
{fileResults.map((file, index) => ( setSelectedFile(file.filename)} > ))}
Source Type Size Matching chunks Average score Owner
{getSourceIcon(file.connector_type)} {file.filename}
{file.mimetype} {file.size ? `${Math.round(file.size / 1024)} KB` : '—'} {file.chunkCount} {file.avgScore.toFixed(2)} {file.owner_name || file.owner || '—'}
)}
)}
) } export default function ProtectedSearchPage() { return ( ) }