Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
Lucas Oliveira
ced7a5ee9f format 2025-10-06 15:35:51 -03:00
Lucas Oliveira
93f91350eb Merge remote-tracking branch 'origin/main' into add-globe-icon-text-html-url 2025-10-06 15:35:48 -03:00
Lucas Oliveira
ad372e7f47 fix formatting 2025-10-06 15:34:03 -03:00
Lucas Oliveira
7301fd8a44 removed wrong docs 2025-10-06 15:33:33 -03:00
Lucas Oliveira
c718e21eea Merge remote-tracking branch 'origin/main' into add-globe-icon-text-html-url 2025-10-06 14:40:46 -03:00
Edwin Jose
5db01049bd
Merge branch 'main' into add-globe-icon-text-html-url 2025-10-06 10:48:14 -04:00
Lucas Oliveira
ae4925cd56 changed page to get connector type as url 2025-10-03 18:06:52 -03:00
Edwin Jose
5654620989 Show globe icon for HTML documents in knowledge page
Updated getSourceIcon to display a globe icon for documents with 'text/html' mimetype, regardless of connector type. This improves visual identification of web-based documents in the grid.
2025-10-03 12:29:46 -04:00

View file

@ -2,14 +2,22 @@
import type { ColDef, GetRowIdParams } from "ag-grid-community"; import type { ColDef, GetRowIdParams } from "ag-grid-community";
import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react"; import { AgGridReact, type CustomCellRendererProps } from "ag-grid-react";
import { Building2, Cloud, HardDrive, Search, Trash2, X } from "lucide-react"; import {
Building2,
Cloud,
Globe,
HardDrive,
Search,
Trash2,
X,
} from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { import {
type ChangeEvent, type ChangeEvent,
useCallback, useCallback,
useEffect, useEffect,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { SiGoogledrive } from "react-icons/si"; import { SiGoogledrive } from "react-icons/si";
import { TbBrandOnedrive } from "react-icons/tb"; import { TbBrandOnedrive } from "react-icons/tb";
@ -31,250 +39,255 @@ import { useDeleteDocument } from "../api/mutations/useDeleteDocument";
// Function to get the appropriate icon for a connector type // Function to get the appropriate icon for a connector type
function getSourceIcon(connectorType?: string) { function getSourceIcon(connectorType?: string) {
switch (connectorType) { switch (connectorType) {
case "google_drive": case "url":
return ( return <Globe className="h-4 w-4 text-muted-foreground flex-shrink-0" />;
<SiGoogledrive className="h-4 w-4 text-foreground flex-shrink-0" /> case "google_drive":
); return (
case "onedrive": <SiGoogledrive className="h-4 w-4 text-foreground flex-shrink-0" />
return ( );
<TbBrandOnedrive className="h-4 w-4 text-foreground flex-shrink-0" /> case "onedrive":
); return (
case "sharepoint": <TbBrandOnedrive className="h-4 w-4 text-foreground flex-shrink-0" />
return <Building2 className="h-4 w-4 text-foreground flex-shrink-0" />; );
case "s3": case "sharepoint":
return <Cloud className="h-4 w-4 text-foreground flex-shrink-0" />; return <Building2 className="h-4 w-4 text-foreground flex-shrink-0" />;
default: case "s3":
return ( return <Cloud className="h-4 w-4 text-foreground flex-shrink-0" />;
<HardDrive className="h-4 w-4 text-muted-foreground flex-shrink-0" /> default:
); return (
} <HardDrive className="h-4 w-4 text-muted-foreground flex-shrink-0" />
);
}
} }
function SearchPage() { function SearchPage() {
const router = useRouter(); const router = useRouter();
const { isMenuOpen, files: taskFiles, refreshTasks } = useTask(); const { isMenuOpen, files: taskFiles, refreshTasks } = useTask();
const { totalTopOffset } = useLayout(); const { totalTopOffset } = useLayout();
const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } = const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } =
useKnowledgeFilter(); useKnowledgeFilter();
const [selectedRows, setSelectedRows] = useState<File[]>([]); const [selectedRows, setSelectedRows] = useState<File[]>([]);
const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false);
const deleteDocumentMutation = useDeleteDocument(); const deleteDocumentMutation = useDeleteDocument();
useEffect(() => { useEffect(() => {
refreshTasks(); refreshTasks();
}, [refreshTasks]); }, [refreshTasks]);
const { data: searchData = [], isFetching } = useGetSearchQuery( const { data: searchData = [], isFetching } = useGetSearchQuery(
parsedFilterData?.query || "*", parsedFilterData?.query || "*",
parsedFilterData, parsedFilterData,
); );
// Convert TaskFiles to File format and merge with backend results // Convert TaskFiles to File format and merge with backend results
const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => { const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => {
return { return {
filename: taskFile.filename, filename: taskFile.filename,
mimetype: taskFile.mimetype, mimetype: taskFile.mimetype,
source_url: taskFile.source_url, source_url: taskFile.source_url,
size: taskFile.size, size: taskFile.size,
connector_type: taskFile.connector_type, connector_type: taskFile.connector_type,
status: taskFile.status, status: taskFile.status,
}; };
}); });
// Create a map of task files by filename for quick lookup // Create a map of task files by filename for quick lookup
const taskFileMap = new Map( const taskFileMap = new Map(
taskFilesAsFiles.map((file) => [file.filename, file]), taskFilesAsFiles.map((file) => [file.filename, file]),
); );
// Override backend files with task file status if they exist // Override backend files with task file status if they exist
const backendFiles = (searchData as File[]) const backendFiles = (searchData as File[])
.map((file) => { .map((file) => {
const taskFile = taskFileMap.get(file.filename); const taskFile = taskFileMap.get(file.filename);
if (taskFile) { if (taskFile) {
// Override backend file with task file data (includes status) // Override backend file with task file data (includes status)
return { ...file, ...taskFile }; return { ...file, ...taskFile };
} }
return file; return file;
}) })
.filter((file) => { .filter((file) => {
// Only filter out files that are currently processing AND in taskFiles // Only filter out files that are currently processing AND in taskFiles
const taskFile = taskFileMap.get(file.filename); const taskFile = taskFileMap.get(file.filename);
return !taskFile || taskFile.status !== "processing"; return !taskFile || taskFile.status !== "processing";
}); });
const filteredTaskFiles = taskFilesAsFiles.filter((taskFile) => { const filteredTaskFiles = taskFilesAsFiles.filter((taskFile) => {
return ( return (
taskFile.status !== "active" && taskFile.status !== "active" &&
!backendFiles.some( !backendFiles.some(
(backendFile) => backendFile.filename === taskFile.filename, (backendFile) => backendFile.filename === taskFile.filename,
) )
); );
}); });
// Combine task files first, then backend files // Combine task files first, then backend files
const fileResults = [...backendFiles, ...filteredTaskFiles]; const fileResults = [...backendFiles, ...filteredTaskFiles];
const handleTableSearch = (e: ChangeEvent<HTMLInputElement>) => { const handleTableSearch = (e: ChangeEvent<HTMLInputElement>) => {
gridRef.current?.api.setGridOption("quickFilterText", e.target.value); gridRef.current?.api.setGridOption("quickFilterText", e.target.value);
}; };
const gridRef = useRef<AgGridReact>(null); const gridRef = useRef<AgGridReact>(null);
const columnDefs = [ const columnDefs = [
{ {
field: "filename", field: "filename",
headerName: "Source", headerName: "Source",
checkboxSelection: (params: CustomCellRendererProps<File>) => checkboxSelection: (params: CustomCellRendererProps<File>) =>
(params?.data?.status || "active") === "active", (params?.data?.status || "active") === "active",
headerCheckboxSelection: true, headerCheckboxSelection: true,
initialFlex: 2, initialFlex: 2,
minWidth: 220, minWidth: 220,
cellRenderer: ({ data, value }: CustomCellRendererProps<File>) => { cellRenderer: ({ data, value }: CustomCellRendererProps<File>) => {
// Read status directly from data on each render // Read status directly from data on each render
const status = data?.status || "active"; const status = data?.status || "active";
const isActive = status === "active"; const isActive = status === "active";
console.log(data?.filename, status, "a"); console.log(data?.filename, status, "a");
return ( return (
<div className="flex items-center overflow-hidden w-full"> <div className="flex items-center overflow-hidden w-full">
<div <div
className={`transition-opacity duration-200 ${isActive ? "w-0" : "w-7"}`} className={`transition-opacity duration-200 ${
></div> isActive ? "w-0" : "w-7"
<button }`}
type="button" ></div>
className="flex items-center gap-2 cursor-pointer hover:text-blue-600 transition-colors text-left flex-1 overflow-hidden" <button
onClick={() => { type="button"
if (!isActive) { className="flex items-center gap-2 cursor-pointer hover:text-blue-600 transition-colors text-left flex-1 overflow-hidden"
return; onClick={() => {
} if (!isActive) {
router.push( return;
`/knowledge/chunks?filename=${encodeURIComponent( }
data?.filename ?? "", router.push(
)}`, `/knowledge/chunks?filename=${encodeURIComponent(
); data?.filename ?? "",
}} )}`,
> );
{getSourceIcon(data?.connector_type)} }}
<span className="font-medium text-foreground truncate"> >
{value} {getSourceIcon(data?.connector_type)}
</span> <span className="font-medium text-foreground truncate">
</button> {value}
</div> </span>
); </button>
}, </div>
}, );
{ },
field: "size", },
headerName: "Size", {
valueFormatter: (params: CustomCellRendererProps<File>) => field: "size",
params.value ? `${Math.round(params.value / 1024)} KB` : "-", headerName: "Size",
}, valueFormatter: (params: CustomCellRendererProps<File>) =>
{ params.value ? `${Math.round(params.value / 1024)} KB` : "-",
field: "mimetype", },
headerName: "Type", {
}, field: "mimetype",
{ headerName: "Type",
field: "owner", },
headerName: "Owner", {
valueFormatter: (params: CustomCellRendererProps<File>) => field: "owner",
params.data?.owner_name || params.data?.owner_email || "—", headerName: "Owner",
}, valueFormatter: (params: CustomCellRendererProps<File>) =>
{ params.data?.owner_name || params.data?.owner_email || "—",
field: "chunkCount", },
headerName: "Chunks", {
valueFormatter: (params: CustomCellRendererProps<File>) => params.data?.chunkCount?.toString() || "-", field: "chunkCount",
}, headerName: "Chunks",
{ valueFormatter: (params: CustomCellRendererProps<File>) =>
field: "avgScore", params.data?.chunkCount?.toString() || "-",
headerName: "Avg score", },
initialFlex: 0.5, {
cellRenderer: ({ value }: CustomCellRendererProps<File>) => { field: "avgScore",
return ( headerName: "Avg score",
<span className="text-xs text-accent-emerald-foreground bg-accent-emerald px-2 py-1 rounded"> initialFlex: 0.5,
{value?.toFixed(2) ?? "-"} cellRenderer: ({ value }: CustomCellRendererProps<File>) => {
</span> return (
); <span className="text-xs text-accent-emerald-foreground bg-accent-emerald px-2 py-1 rounded">
}, {value?.toFixed(2) ?? "-"}
}, </span>
{ );
field: "status", },
headerName: "Status", },
cellRenderer: ({ data }: CustomCellRendererProps<File>) => { {
console.log(data?.filename, data?.status, "b"); field: "status",
// Default to 'active' status if no status is provided headerName: "Status",
const status = data?.status || "active"; cellRenderer: ({ data }: CustomCellRendererProps<File>) => {
return <StatusBadge status={status} />; console.log(data?.filename, data?.status, "b");
}, // Default to 'active' status if no status is provided
}, const status = data?.status || "active";
{ return <StatusBadge status={status} />;
cellRenderer: ({ data }: CustomCellRendererProps<File>) => { },
const status = data?.status || "active"; },
if (status !== "active") { {
return null; cellRenderer: ({ data }: CustomCellRendererProps<File>) => {
} const status = data?.status || "active";
return <KnowledgeActionsDropdown filename={data?.filename || ""} />; if (status !== "active") {
}, return null;
cellStyle: { }
alignItems: "center", return <KnowledgeActionsDropdown filename={data?.filename || ""} />;
display: "flex", },
justifyContent: "center", cellStyle: {
padding: 0, alignItems: "center",
}, display: "flex",
colId: "actions", justifyContent: "center",
filter: false, padding: 0,
minWidth: 0, },
width: 40, colId: "actions",
resizable: false, filter: false,
sortable: false, minWidth: 0,
initialFlex: 0, width: 40,
}, resizable: false,
]; sortable: false,
initialFlex: 0,
},
];
const defaultColDef: ColDef<File> = { const defaultColDef: ColDef<File> = {
resizable: false, resizable: false,
suppressMovable: true, suppressMovable: true,
initialFlex: 1, initialFlex: 1,
minWidth: 100, minWidth: 100,
}; };
const onSelectionChanged = useCallback(() => { const onSelectionChanged = useCallback(() => {
if (gridRef.current) { if (gridRef.current) {
const selectedNodes = gridRef.current.api.getSelectedRows(); const selectedNodes = gridRef.current.api.getSelectedRows();
setSelectedRows(selectedNodes); setSelectedRows(selectedNodes);
} }
}, []); }, []);
const handleBulkDelete = async () => { const handleBulkDelete = async () => {
if (selectedRows.length === 0) return; if (selectedRows.length === 0) return;
try { try {
// Delete each file individually since the API expects one filename at a time // Delete each file individually since the API expects one filename at a time
const deletePromises = selectedRows.map((row) => const deletePromises = selectedRows.map((row) =>
deleteDocumentMutation.mutateAsync({ filename: row.filename }), deleteDocumentMutation.mutateAsync({ filename: row.filename }),
); );
await Promise.all(deletePromises); await Promise.all(deletePromises);
toast.success( toast.success(
`Successfully deleted ${selectedRows.length} document${ `Successfully deleted ${selectedRows.length} document${
selectedRows.length > 1 ? "s" : "" selectedRows.length > 1 ? "s" : ""
}`, }`,
); );
setSelectedRows([]); setSelectedRows([]);
setShowBulkDeleteDialog(false); setShowBulkDeleteDialog(false);
// Clear selection in the grid // Clear selection in the grid
if (gridRef.current) { if (gridRef.current) {
gridRef.current.api.deselectAll(); gridRef.current.api.deselectAll();
} }
} catch (error) { } catch (error) {
toast.error( toast.error(
error instanceof Error error instanceof Error
? error.message ? error.message
: "Failed to delete some documents", : "Failed to delete some documents",
); );
} }
}; };
return ( return (
<div <div
@ -298,37 +311,35 @@ function SearchPage() {
<KnowledgeDropdown variant="button" /> <KnowledgeDropdown variant="button" />
</div> </div>
{/* Search Input Area */} {/* Search Input Area */}
<div className="flex-shrink-0 mb-6 xl:max-w-[75%]"> <div className="flex-shrink-0 mb-6 xl:max-w-[75%]">
<form className="flex gap-3"> <form className="flex gap-3">
<div className="primary-input min-h-10 !flex items-center flex-nowrap focus-within:border-foreground transition-colors !p-[0.3rem]"> <div className="primary-input min-h-10 !flex items-center flex-nowrap focus-within:border-foreground transition-colors !p-[0.3rem]">
{selectedFilter?.name && ( {selectedFilter?.name && (
<div <div
className={`flex items-center gap-1 h-full px-1.5 py-0.5 mr-1 rounded max-w-[25%] ${ className={`flex items-center gap-1 h-full px-1.5 py-0.5 mr-1 rounded max-w-[25%] ${
filterAccentClasses[parsedFilterData?.color || "zinc"] filterAccentClasses[parsedFilterData?.color || "zinc"]
}`} }`}
> >
<span className="truncate">{selectedFilter?.name}</span> <span className="truncate">{selectedFilter?.name}</span>
<X <X
aria-label="Remove filter" aria-label="Remove filter"
className="h-4 w-4 flex-shrink-0 cursor-pointer" className="h-4 w-4 flex-shrink-0 cursor-pointer"
onClick={() => setSelectedFilter(null)} onClick={() => setSelectedFilter(null)}
/> />
</div> </div>
)} )}
<Search <Search className="h-4 w-4 ml-1 flex-shrink-0 text-placeholder-foreground" />
className="h-4 w-4 ml-1 flex-shrink-0 text-placeholder-foreground" <input
/> className="bg-transparent w-full h-full ml-2 focus:outline-none focus-visible:outline-none font-mono placeholder:font-mono"
<input name="search-query"
className="bg-transparent w-full h-full ml-2 focus:outline-none focus-visible:outline-none font-mono placeholder:font-mono" id="search-query"
name="search-query" type="text"
id="search-query" placeholder="Enter your search query..."
type="text" onChange={handleTableSearch}
placeholder="Enter your search query..." />
onChange={handleTableSearch} </div>
/> {/* <Button
</div>
{/* <Button
type="submit" type="submit"
variant="outline" variant="outline"
className="rounded-lg p-0 flex-shrink-0" className="rounded-lg p-0 flex-shrink-0"
@ -339,8 +350,8 @@ function SearchPage() {
<Search className="h-4 w-4" /> <Search className="h-4 w-4" />
)} )}
</Button> */} </Button> */}
{/* //TODO: Implement sync button */} {/* //TODO: Implement sync button */}
{/* <Button {/* <Button
type="button" type="button"
variant="outline" variant="outline"
className="rounded-lg flex-shrink-0" className="rounded-lg flex-shrink-0"
@ -348,69 +359,69 @@ function SearchPage() {
> >
Sync Sync
</Button> */} </Button> */}
{selectedRows.length > 0 && ( {selectedRows.length > 0 && (
<Button <Button
type="button" type="button"
variant="destructive" variant="destructive"
className="rounded-lg flex-shrink-0" className="rounded-lg flex-shrink-0"
onClick={() => setShowBulkDeleteDialog(true)} onClick={() => setShowBulkDeleteDialog(true)}
> >
<Trash2 className="h-4 w-4" /> Delete <Trash2 className="h-4 w-4" /> Delete
</Button> </Button>
)} )}
</form> </form>
</div> </div>
<AgGridReact <AgGridReact
className="w-full overflow-auto" className="w-full overflow-auto"
columnDefs={columnDefs as ColDef<File>[]} columnDefs={columnDefs as ColDef<File>[]}
defaultColDef={defaultColDef} defaultColDef={defaultColDef}
loading={isFetching} loading={isFetching}
ref={gridRef} ref={gridRef}
rowData={fileResults} rowData={fileResults}
rowSelection="multiple" rowSelection="multiple"
rowMultiSelectWithClick={false} rowMultiSelectWithClick={false}
suppressRowClickSelection={true} suppressRowClickSelection={true}
getRowId={(params: GetRowIdParams<File>) => params.data?.filename} getRowId={(params: GetRowIdParams<File>) => params.data?.filename}
domLayout="normal" domLayout="normal"
onSelectionChanged={onSelectionChanged} onSelectionChanged={onSelectionChanged}
noRowsOverlayComponent={() => ( noRowsOverlayComponent={() => (
<div className="text-center pb-[45px]"> <div className="text-center pb-[45px]">
<div className="text-lg text-primary font-semibold"> <div className="text-lg text-primary font-semibold">
No knowledge No knowledge
</div> </div>
<div className="text-sm mt-1 text-muted-foreground"> <div className="text-sm mt-1 text-muted-foreground">
Add files from local or your preferred cloud. Add files from local or your preferred cloud.
</div> </div>
</div> </div>
)} )}
/> />
</div> </div>
{/* Bulk Delete Confirmation Dialog */} {/* Bulk Delete Confirmation Dialog */}
<DeleteConfirmationDialog <DeleteConfirmationDialog
open={showBulkDeleteDialog} open={showBulkDeleteDialog}
onOpenChange={setShowBulkDeleteDialog} onOpenChange={setShowBulkDeleteDialog}
title="Delete Documents" title="Delete Documents"
description={`Are you sure you want to delete ${ description={`Are you sure you want to delete ${
selectedRows.length selectedRows.length
} document${ } document${
selectedRows.length > 1 ? "s" : "" selectedRows.length > 1 ? "s" : ""
}? This will remove all chunks and data associated with these documents. This action cannot be undone. }? This will remove all chunks and data associated with these documents. This action cannot be undone.
Documents to be deleted: Documents to be deleted:
${selectedRows.map((row) => `${row.filename}`).join("\n")}`} ${selectedRows.map((row) => `${row.filename}`).join("\n")}`}
confirmText="Delete All" confirmText="Delete All"
onConfirm={handleBulkDelete} onConfirm={handleBulkDelete}
isLoading={deleteDocumentMutation.isPending} isLoading={deleteDocumentMutation.isPending}
/> />
</div> </div>
); );
} }
export default function ProtectedSearchPage() { export default function ProtectedSearchPage() {
return ( return (
<ProtectedRoute> <ProtectedRoute>
<SearchPage /> <SearchPage />
</ProtectedRoute> </ProtectedRoute>
); );
} }