"use client"; import { type CheckboxSelectionCallbackParams, type ColDef, type GetRowIdParams, themeQuartz, type ValueFormatterParams, } from "ag-grid-community"; import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react"; import { Cloud, FileIcon, Globe } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { KnowledgeDropdown } from "@/components/knowledge-dropdown"; import { ProtectedRoute } from "@/components/protected-route"; import { Button } from "@/components/ui/button"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; import { useTask } from "@/contexts/task-context"; import { type File, useGetSearchQuery } from "../api/queries/useGetSearchQuery"; import "@/components/AgGrid/registerAgGridModules"; import "@/components/AgGrid/agGridStyles.css"; import { toast } from "sonner"; import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdown"; import { KnowledgeSearchInput } from "@/components/knowledge-search-input"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { StatusBadge } from "@/components/ui/status-badge"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { DeleteConfirmationDialog, formatFilesToDelete, } from "../../components/delete-confirmation-dialog"; import GoogleDriveIcon from "../../components/icons/google-drive-logo"; import OneDriveIcon from "../../components/icons/one-drive-logo"; import SharePointIcon from "../../components/icons/share-point-logo"; import { useDeleteDocument } from "../api/mutations/useDeleteDocument"; // 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 "url": return ; case "s3": return ; default: return ( ); } } function SearchPage() { const router = useRouter(); const { files: taskFiles, refreshTasks } = useTask(); const { parsedFilterData, queryOverride } = useKnowledgeFilter(); const [selectedRows, setSelectedRows] = useState([]); const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); const lastErrorRef = useRef(null); const deleteDocumentMutation = useDeleteDocument(); useEffect(() => { refreshTasks(); }, [refreshTasks]); const { data: searchData = [], isFetching, error, isError } = useGetSearchQuery( queryOverride, parsedFilterData, ); // Show toast notification for search errors useEffect(() => { if (isError && error) { const errorMessage = error instanceof Error ? error.message : "Search failed"; // Avoid showing duplicate toasts for the same error if (lastErrorRef.current !== errorMessage) { lastErrorRef.current = errorMessage; toast.error("Search error", { description: errorMessage, duration: 5000, }); } } else if (!isError) { // Reset when query succeeds lastErrorRef.current = null; } }, [isError, error]); // Convert TaskFiles to File format and merge with backend results const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => { return { filename: taskFile.filename, mimetype: taskFile.mimetype, source_url: taskFile.source_url, size: taskFile.size, connector_type: taskFile.connector_type, status: taskFile.status, error: taskFile.error, embedding_model: taskFile.embedding_model, embedding_dimensions: taskFile.embedding_dimensions, }; }); // Create a map of task files by filename for quick lookup const taskFileMap = new Map( taskFilesAsFiles.map((file) => [file.filename, file]), ); // Override backend files with task file status if they exist const backendFiles = (searchData as File[]).map((file) => { const taskFile = taskFileMap.get(file.filename); if (taskFile) { // Override backend file with task file data (includes status) return { ...file, ...taskFile }; } return file; }); const filteredTaskFiles = taskFilesAsFiles.filter((taskFile) => { return ( taskFile.status !== "active" && !backendFiles.some( (backendFile) => backendFile.filename === taskFile.filename, ) ); }); // Combine task files first, then backend files const fileResults = [...backendFiles, ...filteredTaskFiles]; const gridRef = useRef(null); const columnDefs: ColDef[] = [ { field: "filename", headerName: "Source", checkboxSelection: (params: CheckboxSelectionCallbackParams) => (params?.data?.status || "active") === "active", headerCheckboxSelection: true, initialFlex: 2, minWidth: 220, cellRenderer: ({ data, value }: CustomCellRendererProps) => { // Read status directly from data on each render const status = data?.status || "active"; const isActive = status === "active"; return (
); }, }, { field: "size", headerName: "Size", valueFormatter: (params: ValueFormatterParams) => params.value ? `${Math.round(params.value / 1024)} KB` : "-", }, { field: "mimetype", headerName: "Type", }, { field: "owner", headerName: "Owner", valueFormatter: (params: ValueFormatterParams) => params.data?.owner_name || params.data?.owner_email || "—", }, { field: "chunkCount", headerName: "Chunks", valueFormatter: (params: ValueFormatterParams) => params.data?.chunkCount?.toString() || "-", }, { field: "avgScore", headerName: "Avg score", cellRenderer: ({ value }: CustomCellRendererProps) => { return ( {value?.toFixed(2) ?? "-"} ); }, }, { field: "embedding_model", headerName: "Embedding model", minWidth: 200, cellRenderer: ({ data }: CustomCellRendererProps) => ( {data?.embedding_model || "—"} ), }, { field: "embedding_dimensions", headerName: "Dimensions", width: 110, cellRenderer: ({ data }: CustomCellRendererProps) => ( {typeof data?.embedding_dimensions === "number" ? data.embedding_dimensions.toString() : "—"} ), }, { field: "status", headerName: "Status", cellRenderer: ({ data }: CustomCellRendererProps) => { const status = data?.status || "active"; const error = typeof data?.error === "string" && data.error.trim().length > 0 ? data.error.trim() : undefined; if (status === "failed" && error) { return ( Ingestion failed {data?.filename || "Unknown file"}
{error}
); } return ; }, }, { cellRenderer: ({ data }: CustomCellRendererProps) => { const status = data?.status || "active"; if (status !== "active") { return null; } return ; }, cellStyle: { alignItems: "center", display: "flex", justifyContent: "center", padding: 0, }, colId: "actions", filter: false, minWidth: 0, width: 40, resizable: false, sortable: false, initialFlex: 0, }, ]; const defaultColDef: ColDef = { resizable: false, suppressMovable: true, initialFlex: 1, minWidth: 100, }; const onSelectionChanged = useCallback(() => { if (gridRef.current) { const selectedNodes = gridRef.current.api.getSelectedRows(); setSelectedRows(selectedNodes); } }, []); const handleBulkDelete = async () => { if (selectedRows.length === 0) return; try { // Delete each file individually since the API expects one filename at a time const deletePromises = selectedRows.map((row) => deleteDocumentMutation.mutateAsync({ filename: row.filename }), ); await Promise.all(deletePromises); toast.success( `Successfully deleted ${selectedRows.length} document${ selectedRows.length > 1 ? "s" : "" }`, ); setSelectedRows([]); setShowBulkDeleteDialog(false); // Clear selection in the grid if (gridRef.current) { gridRef.current.api.deselectAll(); } } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to delete some documents", ); } }; return ( <>

Project Knowledge

{/* Search Input Area */}
{/* //TODO: Implement sync button */} {/* */} {selectedRows.length > 0 && ( )}
[]} defaultColDef={defaultColDef} loading={isFetching} ref={gridRef} theme={themeQuartz.withParams({ browserColorScheme: "inherit" })} rowData={fileResults} rowSelection="multiple" rowMultiSelectWithClick={false} suppressRowClickSelection={true} getRowId={(params: GetRowIdParams) => params.data?.filename} domLayout="normal" onSelectionChanged={onSelectionChanged} noRowsOverlayComponent={() => (
No knowledge
Add files from local or your preferred cloud.
)} />
{/* Bulk Delete Confirmation Dialog */} 1 ? "Delete Documents" : "Delete Document"} description={`Are you sure you want to delete ${ selectedRows.length } document${ selectedRows.length > 1 ? "s" : "" }? This will remove all chunks and data associated with these documents. This action cannot be undone. Documents to be deleted: ${formatFilesToDelete(selectedRows)}`} confirmText={selectedRows.length > 1 ? "Delete All" : "Delete"} onConfirm={handleBulkDelete} isLoading={deleteDocumentMutation.isPending} /> ); } export default function ProtectedSearchPage() { return ( ); }