@llamaindex/ui
Version:
A comprehensive UI component library built with React, TypeScript, and Tailwind CSS for LlamaIndex applications
581 lines (577 loc) • 21 kB
JavaScript
import { Badge } from './chunk-G2AWGW7W.mjs';
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from './chunk-NSFPZOQT.mjs';
import { ColumnFilter } from './chunk-USB77Q4P.mjs';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from './chunk-MWBISAYH.mjs';
import { Pagination, PaginationContent, PaginationItem, PaginationPrevious, PaginationLink, PaginationNext } from './chunk-MLVSERZP.mjs';
import { DEFAULT_CONFIDENCE_THRESHOLD, useUIConfigStore } from './chunk-QQG6BRXU.mjs';
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from './chunk-Y27WI3E4.mjs';
import { useAgentDataClient } from './chunk-7T7LOAQY.mjs';
import { Button } from './chunk-NLLOGSY3.mjs';
import { cn } from './chunk-MG2ARK3A.mjs';
import { __spreadProps, __spreadValues } from './chunk-FWCSY2DS.mjs';
import { useState, useCallback, useEffect, useMemo } from 'react';
import { ArrowDown, ArrowUp, ArrowUpDown, CheckIcon, XIcon, MoreVerticalIcon, TrashIcon } from 'lucide-react';
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
import { toast } from 'sonner';
function useItemGridData(paginationState, filterFields = {}, sortSpec = void 0) {
const client = useAgentDataClient();
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [totalSize, setTotalSize] = useState(0);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await client.search({
filter: filterFields,
orderBy: sortSpec,
offset: paginationState.page * paginationState.size,
pageSize: paginationState.size,
includeTotal: true
});
setData(response.items || []);
setTotalSize(response.totalSize || 0);
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setLoading(false);
}
}, [
paginationState.page,
paginationState.size,
JSON.stringify(filterFields),
sortSpec,
client
]);
const handleDeleteItem = useCallback(
async (itemId) => {
try {
await client.deleteItem(itemId);
setData(
(prevData) => prevData.filter((item) => String(item.id) !== String(itemId))
);
setTotalSize((prevTotal) => prevTotal - 1);
return { success: true };
} catch (error2) {
console.error("Delete error:", error2);
return {
success: false,
error: "Failed to delete item. Please try again."
};
}
},
[client]
);
useEffect(() => {
fetchData();
}, [fetchData]);
return {
data,
loading,
error,
totalSize,
deleteItem: handleDeleteItem,
fetchData
};
}
function ColumnHeader({
column,
sortState,
onSort,
filterOptions,
selectedFilters,
onFilterChange
}) {
var _a;
const sortKey = (_a = column.sortKey) != null ? _a : column.key;
const sortDirection = (sortState == null ? void 0 : sortState.column) === sortKey ? sortState.direction : null;
return /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
/* @__PURE__ */ jsxs(
"div",
{
className: cn(
"flex items-center justify-center space-x-1",
column.sortable && "cursor-pointer hover:text-primary"
),
onClick: () => column.sortable && (onSort == null ? void 0 : onSort(sortKey)),
children: [
/* @__PURE__ */ jsx("span", { className: "text-base-muted-foreground text-sm font-medium", children: column.header }),
column.sortable && /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: sortDirection === "asc" ? /* @__PURE__ */ jsx(ArrowDown, { size: 16 }) : sortDirection === "desc" ? /* @__PURE__ */ jsx(ArrowUp, { size: 16 }) : /* @__PURE__ */ jsx(ArrowUpDown, { size: 16, className: "opacity-40" }) })
]
}
),
filterOptions && filterOptions.length > 0 && /* @__PURE__ */ jsx(
ColumnFilter,
{
options: filterOptions,
selectedValues: selectedFilters || [],
onFilterChange: (values) => onFilterChange == null ? void 0 : onFilterChange(column.key, values)
}
)
] });
}
var PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
function PaginationControls({
paginationState,
totalSize,
onPaginationChange
}) {
const { page, size } = paginationState;
const totalPages = Math.max(1, Math.ceil(totalSize / size));
function getPageNumbers() {
if (totalPages <= 5) return Array.from({ length: totalPages }, (_, i) => i);
if (page <= 2) return [0, 1, 2, -1, totalPages - 1];
if (page >= totalPages - 3)
return [0, -1, totalPages - 3, totalPages - 2, totalPages - 1];
return [0, -1, page, -1, totalPages - 1];
}
return /* @__PURE__ */ jsx(Pagination, { children: /* @__PURE__ */ jsxs(PaginationContent, { className: "flex w-full items-center justify-between", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsx("span", { className: "text-sm text-base-foreground", children: "Rows per page" }),
/* @__PURE__ */ jsxs(DropdownMenu, { children: [
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { className: "border rounded px-2 py-1 text-sm min-w-[48px] text-left focus:outline-none cursor-pointer", children: size }) }),
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", children: PAGE_SIZE_OPTIONS.map((option) => /* @__PURE__ */ jsx(
DropdownMenuItem,
{
onClick: () => onPaginationChange({ page: 0, size: option }),
className: cn(
"cursor-pointer px-2 py-1 text-sm",
size === option && "bg-accent font-bold text-primary"
),
children: option
},
option
)) })
] })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(
PaginationPrevious,
{
onClick: () => page > 0 && onPaginationChange(__spreadProps(__spreadValues({}, paginationState), { page: page - 1 })),
className: cn(
page === 0 && "pointer-events-none opacity-50",
"text-base-foreground",
"cursor-pointer"
)
}
) }),
getPageNumbers().map(
(num, idx) => num === -1 ? /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx("span", { className: "px-2 text-base-foreground text-xs", children: "..." }) }, "ellipsis-" + idx) : /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(
PaginationLink,
{
isActive: page === num,
onClick: () => onPaginationChange(__spreadProps(__spreadValues({}, paginationState), { page: num })),
className: cn(
"text-base-foreground",
"size-7",
"cursor-pointer"
),
children: num + 1
}
) }, num)
),
/* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(
PaginationNext,
{
onClick: () => page < totalPages - 1 && onPaginationChange(__spreadProps(__spreadValues({}, paginationState), { page: page + 1 })),
className: cn(
page >= totalPages - 1 && "pointer-events-none opacity-50",
"text-base-foreground",
"cursor-pointer"
)
}
) })
] })
] }) });
}
function ItemGrid({
customColumns = [],
onRowClick,
defaultPageSize = 20
}) {
const [paginationState, setPaginationState] = useState({
page: 0,
size: defaultPageSize
});
const [sortState, setSortState] = useState({
column: "created_at",
direction: "desc"
});
const [filters, setFilters] = useState({});
const columns = useMemo(() => {
const finalColumns = [];
finalColumns.push(...customColumns);
return finalColumns;
}, [customColumns]);
const apiFilters = useMemo(() => {
const result = {};
Object.entries(filters).forEach(([columnKey, filterValues]) => {
if (filterValues.length > 0) {
result[columnKey] = { includes: filterValues };
}
});
return result;
}, [filters]);
const apiSort = useMemo(() => {
let result = void 0;
if (sortState.column && sortState.direction) {
result = `${sortState.column} ${sortState.direction}`;
}
return result;
}, [sortState]);
const { data, loading, error, totalSize, deleteItem, fetchData } = useItemGridData(paginationState, apiFilters, apiSort);
const hooks = useMemo(
() => ({
deleteItem,
fetchData
}),
[deleteItem, fetchData]
);
const handleSort = (columnKey) => {
setSortState((prev) => {
if (prev.column === columnKey) {
const newDirection = prev.direction === "asc" ? "desc" : prev.direction === "desc" ? null : "asc";
return { column: columnKey, direction: newDirection };
} else {
return { column: columnKey, direction: "asc" };
}
});
};
const handleFilterChange = (columnKey, values) => {
setFilters((prev) => __spreadProps(__spreadValues({}, prev), {
[columnKey]: values
}));
};
const getFilterOptions = (columnKey) => {
const column = columns.find((col) => col.key === columnKey);
if (!column || !column.filterable) return [];
if (column.filterOptions) {
return column.filterOptions;
}
const values = data.map((item) => {
const value = column.getValue(item);
return String(value || "");
}).filter(Boolean);
return [...new Set(values)].sort();
};
if (loading) {
return /* @__PURE__ */ jsx("div", { className: "flex justify-center items-center p-8", children: /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading items..." }) });
}
if (error) {
return /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/50 bg-destructive/10 p-4", children: /* @__PURE__ */ jsxs("div", { className: "text-sm text-destructive", children: [
"Error loading items: ",
error
] }) });
}
return /* @__PURE__ */ jsxs("div", { className: "w-full space-y-4", children: [
/* @__PURE__ */ jsx("div", { className: "rounded-md border", children: /* @__PURE__ */ jsxs(Table, { className: "table-fixed", children: [
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => /* @__PURE__ */ jsx(
TableHead,
{
className: cn(
"font-medium",
column.key === "actions" && "w-15"
),
children: /* @__PURE__ */ jsx(
ColumnHeader,
{
column,
sortState,
onSort: handleSort,
filterOptions: getFilterOptions(column.key),
selectedFilters: filters[column.key],
onFilterChange: handleFilterChange
}
)
},
column.key
)) }) }),
/* @__PURE__ */ jsx(TableBody, { children: data.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(TableRow, { className: "sr-only", children: columns.map((column) => /* @__PURE__ */ jsx(
TableCell,
{
className: column.key === "actions" ? "w-15" : void 0,
children: /* @__PURE__ */ jsx("div", { className: "h-0 overflow-hidden", children: "placeholder" })
},
column.key
)) }),
/* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
TableCell,
{
colSpan: columns.length,
className: "h-24 text-center text-muted-foreground",
children: "No items found."
}
) })
] }) : data.map((item, rowIndex) => /* @__PURE__ */ jsx(
TableRow,
{
className: cn(
"transition-colors hover:bg-muted/50",
onRowClick && "cursor-pointer"
),
onClick: () => onRowClick == null ? void 0 : onRowClick(item),
children: columns.map((column) => {
const value = column.getValue(item);
return /* @__PURE__ */ jsx(
TableCell,
{
className: column.key === "actions" ? "w-15" : void 0,
children: column.renderCell ? column.renderCell(value, hooks) : /* @__PURE__ */ jsx("span", { className: "text-base-foreground text-sm font-normal leading-none", children: String(value || "-") })
},
column.key
);
})
},
item.id || rowIndex
)) })
] }) }),
/* @__PURE__ */ jsx(
PaginationControls,
{
paginationState,
totalSize,
onPaginationChange: setPaginationState
}
)
] });
}
function ReviewStatusBadge({ value }) {
const statusConfig = {
pending_review: {
label: "Awaiting Review",
variant: "secondary",
className: "bg-amber-100 text-amber-600 hover:bg-amber-200"
},
approved: {
label: "Approved",
variant: "default",
className: "bg-green-100 text-green-600 hover:bg-green-200"
},
rejected: {
label: "Rejected",
variant: "destructive",
className: "bg-red-100 text-red-600 hover:bg-red-200"
}
};
const config = statusConfig[value] || {
label: value,
variant: "outline",
className: ""
};
return /* @__PURE__ */ jsx(Badge, { variant: config.variant, className: config.className, children: config.label });
}
function SyncedIcon({ value }) {
return value ? /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600" }) : /* @__PURE__ */ jsx(XIcon, { className: "w-4 h-4 text-red-600" });
}
function FormattedDate({ value }) {
const time = useMemo(() => {
const date = new Date(value);
return date.toLocaleString();
}, [value]);
return /* @__PURE__ */ jsx("span", { className: "text-base-foreground text-sm font-normal leading-none", children: time });
}
function ActionButton({ onDelete }) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
return /* @__PURE__ */ jsxs("div", { onClick: (e) => e.stopPropagation(), children: [
/* @__PURE__ */ jsxs(DropdownMenu, { children: [
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
className: "h-8 w-8 p-0 cursor-pointer",
children: /* @__PURE__ */ jsx(MoreVerticalIcon, { className: "w-4 h-4" })
}
) }),
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", children: /* @__PURE__ */ jsxs(
DropdownMenuItem,
{
onClick: (e) => {
e.stopPropagation();
setShowDeleteDialog(true);
},
className: "text-destructive focus:text-destructive",
children: [
/* @__PURE__ */ jsx(TrashIcon, { className: "w-4 h-4 mr-2" }),
"Delete"
]
}
) })
] }),
/* @__PURE__ */ jsx(AlertDialog, { open: showDeleteDialog, onOpenChange: setShowDeleteDialog, children: /* @__PURE__ */ jsxs(AlertDialogContent, { children: [
/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
/* @__PURE__ */ jsx(AlertDialogTitle, { children: "Delete Item" }),
/* @__PURE__ */ jsx(AlertDialogDescription, { children: "Are you sure you want to delete this item? This action cannot be undone." })
] }),
/* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
/* @__PURE__ */ jsx(AlertDialogCancel, { className: "cursor-pointer", children: "Cancel" }),
/* @__PURE__ */ jsx(
AlertDialogAction,
{
onClick: () => {
onDelete();
setShowDeleteDialog(false);
},
className: "bg-red-600 text-white hover:bg-red-700 focus:ring-2 focus:ring-red-500 focus:ring-offset-2 cursor-pointer font-medium transition-colors",
children: "Delete"
}
)
] })
] }) })
] });
}
// src/item-grid/built-in-columns.tsx
var STATUS_OPTIONS = [
{ value: "pending_review", label: "In Review" },
{ value: "approved", label: "Accepted" },
{ value: "rejected", label: "Rejected" }
];
var EXTRACTED_DATA_COLUMN_NAMES = [
"fileName",
"status",
"itemsToReview",
"createdAt",
"actions"
];
function getExtractedDataItemsToReviewCount(item, confidenceThreshold = DEFAULT_CONFIDENCE_THRESHOLD) {
const metadata = item.data.field_metadata;
if (!metadata || typeof metadata !== "object") return 0;
let lowConfidenceCount = 0;
const visit = (node) => {
if (!node || typeof node !== "object") return;
if (Array.isArray(node)) {
node.forEach(visit);
return;
}
const obj = node;
const confidence = obj.confidence;
const hasNestedObjectChild = Object.values(obj).some(
(v) => v && typeof v === "object" && !Array.isArray(v)
);
if (typeof confidence === "number" && !hasNestedObjectChild) {
if (confidence < confidenceThreshold) lowConfidenceCount++;
return;
}
Object.values(obj).forEach(visit);
};
visit(metadata);
return lowConfidenceCount;
}
function createExtractedDataColumn(columnName, config, confidenceThreshold = DEFAULT_CONFIDENCE_THRESHOLD) {
let baseColumn;
switch (columnName) {
case "fileName":
baseColumn = {
key: "file_name",
header: "File Name",
getValue: (item) => item.data.file_name,
sortable: true
};
break;
case "status":
baseColumn = {
key: "status",
header: "Status",
getValue: (item) => item.data.status,
renderCell: (value) => /* @__PURE__ */ jsx(ReviewStatusBadge, { value }),
filterable: true,
filterOptions: STATUS_OPTIONS.map((option) => option.value),
sortable: true
};
break;
case "itemsToReview":
baseColumn = {
key: "items_to_review",
header: "Items to Review",
getValue: (item) => getExtractedDataItemsToReviewCount(item, confidenceThreshold),
renderCell: (value) => {
const count = value;
return /* @__PURE__ */ jsx("span", { className: "text-base-foreground text-xs font-normal leading-none", children: count });
},
sortable: false
};
break;
case "createdAt":
baseColumn = {
key: "created_at",
header: "Created At",
getValue: (item) => item.createdAt.toISOString(),
renderCell: (value) => /* @__PURE__ */ jsx(FormattedDate, { value }),
sortable: true
};
break;
case "actions":
baseColumn = {
key: "actions",
header: "",
getValue: (item) => item,
renderCell: (value, hooks) => {
if (!(hooks == null ? void 0 : hooks.deleteItem)) {
return null;
}
const item = value;
const deleteItem = hooks.deleteItem;
return /* @__PURE__ */ jsx(
ActionButton,
{
onDelete: async () => {
const result = await deleteItem(item.id);
if (!result.success && result.error) {
toast.error(result.error);
}
}
}
);
}
};
break;
}
if (!baseColumn) {
throw new Error(`Unknown extracted-data column: ${columnName}`);
}
if (config === false) {
throw new Error(`Column ${columnName} is disabled`);
}
if (config === true) {
return baseColumn;
}
return __spreadValues(__spreadValues({}, baseColumn), config);
}
function ExtractedDataItemGrid({
customColumns = [],
builtInColumns = {},
onRowClick,
defaultPageSize = 20
}) {
const confidenceThreshold = useUIConfigStore(
(state) => state.confidenceThreshold
);
const columns = [];
columns.push(...customColumns);
EXTRACTED_DATA_COLUMN_NAMES.forEach((name) => {
const config = builtInColumns[name];
if (config !== false && config !== void 0) {
try {
const builtInColumn = createExtractedDataColumn(
name,
config,
confidenceThreshold
);
columns.push(builtInColumn);
} catch (e) {
}
}
});
return /* @__PURE__ */ jsx(
ItemGrid,
{
customColumns: columns,
onRowClick,
defaultPageSize
}
);
}
export { ColumnHeader, EXTRACTED_DATA_COLUMN_NAMES, ExtractedDataItemGrid, FormattedDate, ItemGrid, PaginationControls, ReviewStatusBadge, STATUS_OPTIONS, SyncedIcon, createExtractedDataColumn, getExtractedDataItemsToReviewCount, useItemGridData };