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 */} -
-
- -
-

Document Chunks

-

- {decodeURIComponent(filename)} -

-
-
- {!isFetching && chunks.length > 0 && ( - - {chunks.length} chunk{chunks.length !== 1 ? "s" : ""} found - - )} +
+
+ + setSelectAll(!!handleSelectAll) + } + /> + +
+
+ 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 + +
+ +
- - {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 +
+
+
+
+
); }