diff --git a/frontend/src/app/knowledge/chunks/page.tsx b/frontend/src/app/knowledge/chunks/page.tsx index 9385c474..cdc9fcc3 100644 --- a/frontend/src/app/knowledge/chunks/page.tsx +++ b/frontend/src/app/knowledge/chunks/page.tsx @@ -1,17 +1,14 @@ "use client"; import { - Building2, - Cloud, - FileText, - HardDrive, + ArrowLeft, + Copy, + File as FileIcon, Loader2, Search, } from "lucide-react"; -import { Suspense, useCallback, useEffect, useState } from "react"; +import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; -import { SiGoogledrive } from "react-icons/si"; -import { TbBrandOnedrive } from "react-icons/tb"; import { ProtectedRoute } from "@/components/protected-route"; import { Button } from "@/components/ui/button"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; @@ -21,22 +18,16 @@ import { type File, useGetSearchQuery, } from "../../api/queries/useGetSearchQuery"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; -// 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 ; - default: - return ; - } -} +const getFileTypeLabel = (mimetype: string) => { + if (mimetype === "application/pdf") return "PDF"; + if (mimetype === "text/plain") return "Text"; + if (mimetype === "application/msword") return "Word Document"; + return "Unknown"; +}; function ChunksPageContent() { const router = useRouter(); @@ -46,10 +37,47 @@ function ChunksPageContent() { const filename = searchParams.get("filename"); const [chunks, setChunks] = useState([]); + const [chunksFilteredByQuery, setChunksFilteredByQuery] = useState< + ChunkResult[] + >([]); + const [selectedChunks, setSelectedChunks] = useState>(new Set()); + + // Calculate average chunk length + const averageChunkLength = useMemo( + () => + chunks.reduce((acc, chunk) => acc + chunk.text.length, 0) / + chunks.length || 0, + [chunks] + ); + + const [selectAll, setSelectAll] = useState(false); + const [queryInputText, setQueryInputText] = useState( + parsedFilterData?.query ?? "" + ); // Use the same search query as the knowledge page, but we'll filter for the specific file const { data = [], isFetching } = useGetSearchQuery("*", parsedFilterData); + useEffect(() => { + if (queryInputText === "") { + setChunksFilteredByQuery(chunks); + } else { + setChunksFilteredByQuery( + chunks.filter((chunk) => + chunk.text.toLowerCase().includes(queryInputText.toLowerCase()) + ) + ); + } + }, [queryInputText, chunks]); + + const handleCopy = useCallback((text: string) => { + navigator.clipboard.writeText(text); + }, []); + + const fileData = (data as File[]).find( + (file: File) => file.filename === filename + ); + // Extract chunks for the specific file useEffect(() => { if (!filename || !(data as File[]).length) { @@ -57,16 +85,37 @@ function ChunksPageContent() { return; } - const fileData = (data as File[]).find( - (file: File) => file.filename === filename - ); setChunks(fileData?.chunks || []); }, [data, filename]); + // Set selected state for all checkboxes when selectAll changes + useEffect(() => { + if (selectAll) { + setSelectedChunks(new Set(chunks.map((_, index) => index))); + } else { + setSelectedChunks(new Set()); + } + }, [selectAll, setSelectedChunks, chunks]); + const handleBack = useCallback(() => { - router.back(); + router.push("/knowledge"); }, [router]); + const handleChunkCardCheckboxChange = useCallback( + (index: number) => { + setSelectedChunks((prevSelected) => { + const newSelected = new Set(prevSelected); + if (newSelected.has(index)) { + newSelected.delete(index); + } else { + newSelected.add(index); + } + return newSelected; + }); + }, + [setSelectedChunks] + ); + if (!filename) { return ( @@ -83,7 +132,7 @@ function ChunksPageContent() { return ( {/* Header */} - - - - ← Back + + + + + + + {filename.replace(/\.[^/.]+$/, "")} + - - Document Chunks - - {decodeURIComponent(filename)} - - - - {!isFetching && chunks.length > 0 && ( - - {chunks.length} chunk{chunks.length !== 1 ? "s" : ""} found - - )} + + + + setSelectAll(!!handleSelectAll) + } + /> + + Select all + + + + setQueryInputText(e.target.value)} + placeholder="Search chunks..." + className="flex-1 bg-muted/20 rounded-lg border border-border/50 px-4 py-3 focus-visible:ring-1 focus-visible:ring-ring" + /> + + + + @@ -147,41 +214,130 @@ function ChunksPageContent() { ) : ( - {chunks.map((chunk, index) => ( + {chunksFilteredByQuery.map((chunk, index) => ( - - - - {chunk.filename} + + + + handleChunkCardCheckboxChange(index) + } + /> + + + Chunk {chunk.page} - {chunk.connector_type && ( - - {getSourceIcon(chunk.connector_type)} - - )} + + {chunk.text.length} chars + + + handleCopy(chunk.text)} + variant="ghost" + size="xs" + > + + + - - {chunk.score.toFixed(2)} - + + {/* TODO: Update to use active toggle */} + {/* + + Active + */} - - {chunk.mimetype} - Page {chunk.page} - {chunk.owner_name && Owner: {chunk.owner_name}} - - + {chunk.text} - + ))} )} + {/* Right panel - Summary (TODO), Technical details, */} + + + Technical details + + + Total chunks + + {chunks.length} + + + + Avg length + + {averageChunkLength.toFixed(0)} chars + + + {/* TODO: Uncomment after data is available */} + {/* + Process time + + + + + Model + + + */} + + + + Original document + + + Name + + {fileData?.filename} + + + + Type + + {fileData ? getFileTypeLabel(fileData.mimetype) : "Unknown"} + + + + Size + + {fileData?.size + ? `${Math.round(fileData.size / 1024)} KB` + : "Unknown"} + + + + Uploaded + + N/A + + + {/* TODO: Uncomment after data is available */} + {/* + Source + + */} + + Updated + + N/A + + + + + ); }
- {decodeURIComponent(filename)} -
+
{chunk.text} - +