diff --git a/frontend/src/app/api/queries/useGetSearchQuery.ts b/frontend/src/app/api/queries/useGetSearchQuery.ts index 9928af3d..725635f8 100644 --- a/frontend/src/app/api/queries/useGetSearchQuery.ts +++ b/frontend/src/app/api/queries/useGetSearchQuery.ts @@ -42,6 +42,7 @@ export interface File { owner_email: string; size: number; connector_type: string; + status?: "processing" | "active" | "unavailable" | "hidden" | "sync"; chunks: ChunkResult[]; } diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx index b23acf84..978c447d 100644 --- a/frontend/src/app/knowledge/page.tsx +++ b/frontend/src/app/knowledge/page.tsx @@ -33,6 +33,7 @@ import { KnowledgeActionsDropdown } from "@/components/knowledge-actions-dropdow import { DeleteConfirmationDialog } from "../../../components/confirmation-dialog"; import { useDeleteDocument } from "../api/mutations/useDeleteDocument"; import { toast } from "sonner"; +import { StatusBadge } from "@/components/ui/status-badge"; // Function to get the appropriate icon for a connector type function getSourceIcon(connectorType?: string) { @@ -127,7 +128,7 @@ function SearchPage() { { field: "size", headerName: "Size", - valueFormatter: (params) => + valueFormatter: params => params.value ? `${Math.round(params.value / 1024)} KB` : "-", }, { @@ -137,7 +138,7 @@ function SearchPage() { { field: "owner", headerName: "Owner", - valueFormatter: (params) => + valueFormatter: params => params.data?.owner_name || params.data?.owner_email || "—", }, { @@ -155,6 +156,15 @@ function SearchPage() { ); }, }, + { + field: "status", + headerName: "Status", + cellRenderer: ({ data }: CustomCellRendererProps) => { + // Default to 'active' status if no status is provided + const status = data?.status || "processing"; + return ; + }, + }, { cellRenderer: ({ data }: CustomCellRendererProps) => { return ; @@ -195,7 +205,7 @@ function SearchPage() { try { // 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 }) ); @@ -252,7 +262,7 @@ function SearchPage() { type="text" defaultValue={parsedFilterData?.query} value={queryInputText} - onChange={(e) => setQueryInputText(e.target.value)} + onChange={e => setQueryInputText(e.target.value)} placeholder="Search your documents..." className="flex-1 bg-muted/20 rounded-lg border border-border/50 px-4 py-3 focus-visible:ring-1 focus-visible:ring-ring" /> @@ -297,7 +307,7 @@ function SearchPage() { rowSelection="multiple" rowMultiSelectWithClick={false} suppressRowClickSelection={true} - getRowId={(params) => params.data.filename} + getRowId={params => params.data.filename} onSelectionChanged={onSelectionChanged} suppressHorizontalScroll={false} noRowsOverlayComponent={() => ( @@ -326,7 +336,7 @@ function SearchPage() { }? This will remove all chunks and data associated with these documents. This action cannot be undone. Documents to be deleted: -${selectedRows.map((row) => `• ${row.filename}`).join("\n")}`} +${selectedRows.map(row => `• ${row.filename}`).join("\n")}`} confirmText="Delete All" onConfirm={handleBulkDelete} isLoading={deleteDocumentMutation.isPending} diff --git a/frontend/src/components/ui/animated-processing-icon.tsx b/frontend/src/components/ui/animated-processing-icon.tsx new file mode 100644 index 00000000..eb36b2ab --- /dev/null +++ b/frontend/src/components/ui/animated-processing-icon.tsx @@ -0,0 +1,49 @@ +interface AnimatedProcessingIconProps { + className?: string; + size?: number; +} + +export const AnimatedProcessingIcon = ({ + className = "", + size = 10, +}: AnimatedProcessingIconProps) => { + const width = Math.round((size * 6) / 10); + const height = size; + + return ( + + + + + + + + + ); +}; diff --git a/frontend/src/components/ui/status-badge.tsx b/frontend/src/components/ui/status-badge.tsx new file mode 100644 index 00000000..eaba42cb --- /dev/null +++ b/frontend/src/components/ui/status-badge.tsx @@ -0,0 +1,53 @@ +import { AnimatedProcessingIcon } from "./animated-processing-icon"; + +export type Status = + | "processing" + | "active" + | "unavailable" + | "hidden" + | "sync"; + +interface StatusBadgeProps { + status: Status; + className?: string; +} + +const statusConfig = { + processing: { + label: "Processing", + className: "text-muted-foreground dark:text-muted-foreground ", + }, + active: { + label: "Active", + className: "text-emerald-600 dark:text-emerald-400 ", + }, + unavailable: { + label: "Unavailable", + className: "text-red-600 dark:text-red-400 ", + }, + hidden: { + label: "Hidden", + className: "text-zinc-400 dark:text-zinc-500 ", + }, + sync: { + label: "Sync", + className: "text-amber-700 dark:text-amber-300 underline", + }, +}; + +export const StatusBadge = ({ status, className }: StatusBadgeProps) => { + const config = statusConfig[status]; + + return ( +
+ {status === "processing" && ( + + )} + {config.label} +
+ ); +};