diff --git a/node_modules/.cache/prettier/.prettier-caches/37bd945444dc76999f7aface662ff267baf1dbca.json b/node_modules/.cache/prettier/.prettier-caches/37bd945444dc76999f7aface662ff267baf1dbca.json new file mode 100644 index 000000000..e94f98f91 --- /dev/null +++ b/node_modules/.cache/prettier/.prettier-caches/37bd945444dc76999f7aface662ff267baf1dbca.json @@ -0,0 +1 @@ +{"c301dd3ad9b4036af1d031ecc966d2f02ae1eda4":{"files":{"surfsense_web/hooks/use-documents.ts":["J01fJFm4gXaHAA83Vu5dtOmk/sw=",true],"surfsense_web/components/chat/DocumentsDataTable.tsx":["wgAyJblucK9D3MKKwPe6W9kZphk=",true]},"modified":1753499058926}} \ No newline at end of file diff --git a/surfsense_web/components/chat/DocumentsDataTable.tsx b/surfsense_web/components/chat/DocumentsDataTable.tsx index 6041227a6..7e441bb28 100644 --- a/surfsense_web/components/chat/DocumentsDataTable.tsx +++ b/surfsense_web/components/chat/DocumentsDataTable.tsx @@ -2,501 +2,501 @@ import * as React from "react"; import { - ColumnDef, - ColumnFiltersState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - SortingState, - useReactTable, - VisibilityState, + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, } from "@tanstack/react-table"; import { ArrowUpDown, Calendar, FileText, Search } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Document, DocumentType } from "@/hooks/use-documents"; interface DocumentsDataTableProps { - documents: Document[]; - onSelectionChange: (documents: Document[]) => void; - onDone: () => void; - initialSelectedDocuments?: Document[]; + documents: Document[]; + onSelectionChange: (documents: Document[]) => void; + onDone: () => void; + initialSelectedDocuments?: Document[]; } const DOCUMENT_TYPES: (DocumentType | "ALL")[] = [ - "ALL", - "FILE", - "EXTENSION", - "CRAWLED_URL", - "YOUTUBE_VIDEO", - "SLACK_CONNECTOR", - "NOTION_CONNECTOR", - "GITHUB_CONNECTOR", - "LINEAR_CONNECTOR", - "DISCORD_CONNECTOR", + "ALL", + "FILE", + "EXTENSION", + "CRAWLED_URL", + "YOUTUBE_VIDEO", + "SLACK_CONNECTOR", + "NOTION_CONNECTOR", + "GITHUB_CONNECTOR", + "LINEAR_CONNECTOR", + "DISCORD_CONNECTOR", + "JIRA_CONNECTOR", ]; const getDocumentTypeColor = (type: DocumentType) => { - const colors = { - FILE: "bg-blue-50 text-blue-700 border-blue-200", - EXTENSION: "bg-green-50 text-green-700 border-green-200", - CRAWLED_URL: "bg-purple-50 text-purple-700 border-purple-200", - YOUTUBE_VIDEO: "bg-red-50 text-red-700 border-red-200", - SLACK_CONNECTOR: "bg-yellow-50 text-yellow-700 border-yellow-200", - NOTION_CONNECTOR: "bg-indigo-50 text-indigo-700 border-indigo-200", - GITHUB_CONNECTOR: "bg-gray-50 text-gray-700 border-gray-200", - LINEAR_CONNECTOR: "bg-pink-50 text-pink-700 border-pink-200", - DISCORD_CONNECTOR: "bg-violet-50 text-violet-700 border-violet-200", - }; - return colors[type] || "bg-gray-50 text-gray-700 border-gray-200"; + const colors = { + FILE: "bg-blue-50 text-blue-700 border-blue-200", + EXTENSION: "bg-green-50 text-green-700 border-green-200", + CRAWLED_URL: "bg-purple-50 text-purple-700 border-purple-200", + YOUTUBE_VIDEO: "bg-red-50 text-red-700 border-red-200", + SLACK_CONNECTOR: "bg-yellow-50 text-yellow-700 border-yellow-200", + NOTION_CONNECTOR: "bg-indigo-50 text-indigo-700 border-indigo-200", + GITHUB_CONNECTOR: "bg-gray-50 text-gray-700 border-gray-200", + LINEAR_CONNECTOR: "bg-pink-50 text-pink-700 border-pink-200", + DISCORD_CONNECTOR: "bg-violet-50 text-violet-700 border-violet-200", + JIRA_CONNECTOR: "bg-orange-50 text-orange-700 border-orange-200", + }; + return colors[type] || "bg-gray-50 text-gray-700 border-gray-200"; }; const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> - ), - enableSorting: false, - enableHiding: false, - size: 40, - }, - { - accessorKey: "title", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const title = row.getValue("title") as string; - return ( -
- {title} -
- ); - }, - }, - { - accessorKey: "document_type", - header: "Type", - cell: ({ row }) => { - const type = row.getValue("document_type") as DocumentType; - return ( - - {type.replace(/_/g, " ")} - {type.split("_")[0]} - - ); - }, - size: 80, - meta: { - className: "hidden sm:table-cell", - }, - }, - { - accessorKey: "content", - header: "Preview", - cell: ({ row }) => { - const content = row.getValue("content") as string; - return ( -
- {content.substring(0, 30)}... - - {content.substring(0, 100)}... - -
- ); - }, - enableSorting: false, - meta: { - className: "hidden md:table-cell", - }, - }, - { - accessorKey: "created_at", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const date = new Date(row.getValue("created_at")); - return ( -
- - {date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - })} - - - {date.toLocaleDateString("en-US", { - month: "numeric", - day: "numeric", - })} - -
- ); - }, - size: 80, - }, + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + { + accessorKey: "title", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const title = row.getValue("title") as string; + return ( +
+ {title} +
+ ); + }, + }, + { + accessorKey: "document_type", + header: "Type", + cell: ({ row }) => { + const type = row.getValue("document_type") as DocumentType; + return ( + + {type.replace(/_/g, " ")} + {type.split("_")[0]} + + ); + }, + size: 80, + meta: { + className: "hidden sm:table-cell", + }, + }, + { + accessorKey: "content", + header: "Preview", + cell: ({ row }) => { + const content = row.getValue("content") as string; + return ( +
+ {content.substring(0, 30)}... + + {content.substring(0, 100)}... + +
+ ); + }, + enableSorting: false, + meta: { + className: "hidden md:table-cell", + }, + }, + { + accessorKey: "created_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const date = new Date(row.getValue("created_at")); + return ( +
+ + {date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + + + {date.toLocaleDateString("en-US", { + month: "numeric", + day: "numeric", + })} + +
+ ); + }, + size: 80, + }, ]; export function DocumentsDataTable({ - documents, - onSelectionChange, - onDone, - initialSelectedDocuments = [], + documents, + onSelectionChange, + onDone, + initialSelectedDocuments = [], }: DocumentsDataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [documentTypeFilter, setDocumentTypeFilter] = React.useState< - DocumentType | "ALL" - >("ALL"); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [documentTypeFilter, setDocumentTypeFilter] = React.useState< + DocumentType | "ALL" + >("ALL"); - // Memoize initial row selection to prevent infinite loops - const initialRowSelection = React.useMemo(() => { - if (!documents.length || !initialSelectedDocuments.length) return {}; + // Memoize initial row selection to prevent infinite loops + const initialRowSelection = React.useMemo(() => { + if (!documents.length || !initialSelectedDocuments.length) return {}; - const selection: Record = {}; - initialSelectedDocuments.forEach((selectedDoc) => { - const docIndex = documents.findIndex((doc) => doc.id === selectedDoc.id); - if (docIndex !== -1) { - selection[docIndex.toString()] = true; - } - }); - return selection; - }, [documents, initialSelectedDocuments]); + const selection: Record = {}; + initialSelectedDocuments.forEach((selectedDoc) => { + selection[selectedDoc.id] = true; + }); + return selection; + }, [documents, initialSelectedDocuments]); - const [rowSelection, setRowSelection] = React.useState< - Record - >({}); + const [rowSelection, setRowSelection] = React.useState< + Record + >({}); - // Only update row selection when initialRowSelection actually changes and is not empty - React.useEffect(() => { - const hasChanges = - JSON.stringify(rowSelection) !== JSON.stringify(initialRowSelection); - if (hasChanges && Object.keys(initialRowSelection).length > 0) { - setRowSelection(initialRowSelection); - } - }, [initialRowSelection]); + // Only update row selection when initialRowSelection actually changes and is not empty + React.useEffect(() => { + const hasChanges = + JSON.stringify(rowSelection) !== JSON.stringify(initialRowSelection); + if (hasChanges && Object.keys(initialRowSelection).length > 0) { + setRowSelection(initialRowSelection); + } + }, [initialRowSelection]); - // Initialize row selection on mount - React.useEffect(() => { - if ( - Object.keys(rowSelection).length === 0 && - Object.keys(initialRowSelection).length > 0 - ) { - setRowSelection(initialRowSelection); - } - }, []); + // Initialize row selection on mount + React.useEffect(() => { + if ( + Object.keys(rowSelection).length === 0 && + Object.keys(initialRowSelection).length > 0 + ) { + setRowSelection(initialRowSelection); + } + }, []); - const filteredDocuments = React.useMemo(() => { - if (documentTypeFilter === "ALL") return documents; - return documents.filter((doc) => doc.document_type === documentTypeFilter); - }, [documents, documentTypeFilter]); + const filteredDocuments = React.useMemo(() => { + if (documentTypeFilter === "ALL") return documents; + return documents.filter((doc) => doc.document_type === documentTypeFilter); + }, [documents, documentTypeFilter]); - const table = useReactTable({ - data: filteredDocuments, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - initialState: { pagination: { pageSize: 10 } }, - state: { sorting, columnFilters, columnVisibility, rowSelection }, - }); + const table = useReactTable({ + data: filteredDocuments, + columns, + getRowId: (row) => row.id.toString(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + initialState: { pagination: { pageSize: 10 } }, + state: { sorting, columnFilters, columnVisibility, rowSelection }, + }); - React.useEffect(() => { - const selectedRows = table.getFilteredSelectedRowModel().rows; - const selectedDocuments = selectedRows.map((row) => row.original); - onSelectionChange(selectedDocuments); - }, [rowSelection, onSelectionChange, table]); + React.useEffect(() => { + const selectedRows = table.getFilteredSelectedRowModel().rows; + const selectedDocuments = selectedRows.map((row) => row.original); + onSelectionChange(selectedDocuments); + }, [rowSelection, onSelectionChange, table]); - const handleClearAll = () => setRowSelection({}); + const handleClearAll = () => setRowSelection({}); - const handleSelectPage = () => { - const currentPageRows = table.getRowModel().rows; - const newSelection = { ...rowSelection }; - currentPageRows.forEach((row) => { - newSelection[row.id] = true; - }); - setRowSelection(newSelection); - }; + const handleSelectPage = () => { + const currentPageRows = table.getRowModel().rows; + const newSelection = { ...rowSelection }; + currentPageRows.forEach((row) => { + newSelection[row.id] = true; + }); + setRowSelection(newSelection); + }; - const handleSelectAllFiltered = () => { - const allFilteredRows = table.getFilteredRowModel().rows; - const newSelection: Record = {}; - allFilteredRows.forEach((row) => { - newSelection[row.id] = true; - }); - setRowSelection(newSelection); - }; + const handleSelectAllFiltered = () => { + const allFilteredRows = table.getFilteredRowModel().rows; + const newSelection: Record = {}; + allFilteredRows.forEach((row) => { + newSelection[row.id] = true; + }); + setRowSelection(newSelection); + }; - const selectedCount = table.getFilteredSelectedRowModel().rows.length; - const totalFiltered = table.getFilteredRowModel().rows.length; + const selectedCount = table.getFilteredSelectedRowModel().rows.length; + const totalFiltered = table.getFilteredRowModel().rows.length; - return ( -
- {/* Header Controls */} -
- {/* Search and Filter Row */} -
-
- - - table.getColumn("title")?.setFilterValue(event.target.value) - } - className="pl-10 text-sm" - /> -
- -
+ return ( +
+ {/* Header Controls */} +
+ {/* Search and Filter Row */} +
+
+ + + table.getColumn("title")?.setFilterValue(event.target.value) + } + className="pl-10 text-sm" + /> +
+ +
- {/* Action Controls Row */} -
-
- - {selectedCount} of {totalFiltered} selected - -
-
- - - - -
-
- -
-
+ {/* Action Controls Row */} +
+
+ + {selectedCount} of {totalFiltered} selected + +
+
+ + + + +
+
+ +
+
- {/* Table Container */} -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - No documents found. - - - )} - -
-
-
+ {/* Table Container */} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No documents found. + + + )} + +
+
+
- {/* Footer Pagination */} -
-
- Showing{" "} - {table.getState().pagination.pageIndex * - table.getState().pagination.pageSize + - 1}{" "} - to{" "} - {Math.min( - (table.getState().pagination.pageIndex + 1) * - table.getState().pagination.pageSize, - table.getFilteredRowModel().rows.length, - )}{" "} - of {table.getFilteredRowModel().rows.length} documents -
-
- -
- Page - {table.getState().pagination.pageIndex + 1} - of - {table.getPageCount()} -
- -
-
-
- ); + {/* Footer Pagination */} +
+
+ Showing{" "} + {table.getState().pagination.pageIndex * + table.getState().pagination.pageSize + + 1}{" "} + to{" "} + {Math.min( + (table.getState().pagination.pageIndex + 1) * + table.getState().pagination.pageSize, + table.getFilteredRowModel().rows.length, + )}{" "} + of {table.getFilteredRowModel().rows.length} documents +
+
+ +
+ Page + {table.getState().pagination.pageIndex + 1} + of + {table.getPageCount()} +
+ +
+
+
+ ); } diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index a7d373196..09b2d3f95 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -3,119 +3,120 @@ import { useState, useEffect, useCallback } from "react"; import { toast } from "sonner"; export interface Document { - id: number; - title: string; - document_type: DocumentType; - document_metadata: any; - content: string; - created_at: string; - search_space_id: number; + id: number; + title: string; + document_type: DocumentType; + document_metadata: any; + content: string; + created_at: string; + search_space_id: number; } export type DocumentType = - | "EXTENSION" - | "CRAWLED_URL" - | "SLACK_CONNECTOR" - | "NOTION_CONNECTOR" - | "FILE" - | "YOUTUBE_VIDEO" - | "GITHUB_CONNECTOR" - | "LINEAR_CONNECTOR" - | "DISCORD_CONNECTOR"; + | "EXTENSION" + | "CRAWLED_URL" + | "SLACK_CONNECTOR" + | "NOTION_CONNECTOR" + | "FILE" + | "YOUTUBE_VIDEO" + | "GITHUB_CONNECTOR" + | "LINEAR_CONNECTOR" + | "DISCORD_CONNECTOR" + | "JIRA_CONNECTOR"; export function useDocuments(searchSpaceId: number, lazy: boolean = false) { - const [documents, setDocuments] = useState([]); - const [loading, setLoading] = useState(!lazy); // Don't show loading initially for lazy mode - const [error, setError] = useState(null); - const [isLoaded, setIsLoaded] = useState(false); // Memoization flag + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(!lazy); // Don't show loading initially for lazy mode + const [error, setError] = useState(null); + const [isLoaded, setIsLoaded] = useState(false); // Memoization flag - const fetchDocuments = useCallback(async () => { - if (isLoaded && lazy) return; // Avoid redundant calls in lazy mode + const fetchDocuments = useCallback(async () => { + if (isLoaded && lazy) return; // Avoid redundant calls in lazy mode - try { - setLoading(true); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents?search_space_id=${searchSpaceId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem( - "surfsense_bearer_token" - )}`, - }, - method: "GET", - } - ); - - if (!response.ok) { - toast.error("Failed to fetch documents"); - throw new Error("Failed to fetch documents"); - } - - const data = await response.json(); - setDocuments(data); - setError(null); - setIsLoaded(true); - } catch (err: any) { - setError(err.message || "Failed to fetch documents"); - console.error("Error fetching documents:", err); - } finally { - setLoading(false); - } - }, [searchSpaceId, isLoaded, lazy]); - - useEffect(() => { - if (!lazy && searchSpaceId) { - fetchDocuments(); - } - }, [searchSpaceId, lazy, fetchDocuments]); - - // Function to refresh the documents list - const refreshDocuments = useCallback(async () => { - setIsLoaded(false); // Reset memoization flag to allow refetch - await fetchDocuments(); - }, [fetchDocuments]); - - // Function to delete a document - const deleteDocument = useCallback( - async (documentId: number) => { - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem( - "surfsense_bearer_token" - )}`, - }, - method: "DELETE", - } - ); - - if (!response.ok) { - toast.error("Failed to delete document"); - throw new Error("Failed to delete document"); - } - - toast.success("Document deleted successfully"); - // Update the local state after successful deletion - setDocuments(documents.filter((doc) => doc.id !== documentId)); - return true; - } catch (err: any) { - toast.error(err.message || "Failed to delete document"); - console.error("Error deleting document:", err); - return false; - } + try { + setLoading(true); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents?search_space_id=${searchSpaceId}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem( + "surfsense_bearer_token", + )}`, + }, + method: "GET", }, - [documents] - ); + ); - return { - documents, - loading, - error, - isLoaded, - fetchDocuments, // Manual fetch function for lazy mode - refreshDocuments, - deleteDocument, - }; + if (!response.ok) { + toast.error("Failed to fetch documents"); + throw new Error("Failed to fetch documents"); + } + + const data = await response.json(); + setDocuments(data); + setError(null); + setIsLoaded(true); + } catch (err: any) { + setError(err.message || "Failed to fetch documents"); + console.error("Error fetching documents:", err); + } finally { + setLoading(false); + } + }, [searchSpaceId, isLoaded, lazy]); + + useEffect(() => { + if (!lazy && searchSpaceId) { + fetchDocuments(); + } + }, [searchSpaceId, lazy, fetchDocuments]); + + // Function to refresh the documents list + const refreshDocuments = useCallback(async () => { + setIsLoaded(false); // Reset memoization flag to allow refetch + await fetchDocuments(); + }, [fetchDocuments]); + + // Function to delete a document + const deleteDocument = useCallback( + async (documentId: number) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem( + "surfsense_bearer_token", + )}`, + }, + method: "DELETE", + }, + ); + + if (!response.ok) { + toast.error("Failed to delete document"); + throw new Error("Failed to delete document"); + } + + toast.success("Document deleted successfully"); + // Update the local state after successful deletion + setDocuments(documents.filter((doc) => doc.id !== documentId)); + return true; + } catch (err: any) { + toast.error(err.message || "Failed to delete document"); + console.error("Error deleting document:", err); + return false; + } + }, + [documents], + ); + + return { + documents, + loading, + error, + isLoaded, + fetchDocuments, // Manual fetch function for lazy mode + refreshDocuments, + deleteDocument, + }; }