import { useCallback, useState, useMemo, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { TrashIcon, MessageSquare, ExternalLink } from 'lucide-react'; import { OGDialog, useToastContext, OGDialogTemplate, OGDialogTrigger, OGDialogContent, useMediaQuery, OGDialogHeader, OGDialogTitle, TooltipAnchor, DataTable, Spinner, Button, Label, } from '@librechat/client'; import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider'; import type { ColumnDef, SortingState } from '@tanstack/react-table'; import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provider'; import { NotificationSeverity } from '~/common'; import { formatDate, cn } from '~/utils'; import { useLocalize } from '~/hooks'; const DEFAULT_PARAMS: SharedLinksListParams = { pageSize: 25, isPublic: true, sortBy: 'createdAt', sortDirection: 'desc', search: '', }; type SortKey = 'createdAt' | 'title'; const isSortKey = (v: string): v is SortKey => v === 'createdAt' || v === 'title'; const defaultSort: SortingState = [ { id: 'createdAt', desc: true, }, ]; type TableColumn = ColumnDef & { meta?: { className?: string; desktopOnly?: boolean; }; }; export default function SharedLinks() { const localize = useLocalize(); const { showToast } = useToastContext(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const [isOpen, setIsOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [deleteRow, setDeleteRow] = useState(null); const [queryParams, setQueryParams] = useState(DEFAULT_PARAMS); const [sorting, setSorting] = useState(defaultSort); const [searchValue, setSearchValue] = useState(''); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } = useSharedLinksQuery(queryParams, { enabled: isOpen, keepPreviousData: true, staleTime: 30 * 1000, refetchOnWindowFocus: false, refetchOnMount: false, }); const [allKnownLinks, setAllKnownLinks] = useState([]); const handleSearchChange = useCallback((value: string) => { setSearchValue(value); setAllKnownLinks([]); setQueryParams((prev) => ({ ...prev, search: value, })); }, []); const handleSortingChange = useCallback( (updater: SortingState | ((old: SortingState) => SortingState)) => { setSorting((prev) => { const next = typeof updater === 'function' ? updater(prev) : updater; const coerced = next; const primary = coerced[0]; if (data?.pages) { const currentFlattened = data.pages.flatMap((page) => page?.links?.filter(Boolean) ?? []); setAllKnownLinks(currentFlattened); } setQueryParams((p) => { let sortBy: SortKey; let sortDirection: 'asc' | 'desc'; if (primary && isSortKey(primary.id)) { sortBy = primary.id; sortDirection = primary.desc ? 'desc' : 'asc'; } else { sortBy = 'createdAt'; sortDirection = 'desc'; } const newParams = { ...p, sortBy, sortDirection, }; return newParams; }); return coerced; }); }, [setQueryParams, data?.pages], ); useEffect(() => { if (!data?.pages) return; const newFlattened = data.pages.flatMap((page) => page?.links?.filter(Boolean) ?? []); const toAdd = newFlattened.filter( (link: SharedLinkItem) => !allKnownLinks.some((known) => known.shareId === link.shareId), ); if (toAdd.length > 0) { setAllKnownLinks((prev) => [...prev, ...toAdd]); } }, [data?.pages]); const displayData = useMemo(() => { const primary = sorting[0]; if (!primary || allKnownLinks.length === 0) return allKnownLinks; return [...allKnownLinks].sort((a: SharedLinkItem, b: SharedLinkItem) => { let compare: number; if (primary.id === 'createdAt') { const aDate = new Date(a.createdAt || 0); const bDate = new Date(b.createdAt || 0); compare = aDate.getTime() - bDate.getTime(); } else if (primary.id === 'title') { compare = (a.title || '').localeCompare(b.title || ''); } else { return 0; } return primary.desc ? -compare : compare; }); }, [allKnownLinks, sorting]); const deleteMutation = useDeleteSharedLinkMutation({ onSuccess: (data, variables) => { const { shareId } = variables; setAllKnownLinks((prev) => prev.filter((link) => link.shareId !== shareId)); showToast({ message: localize('com_ui_shared_link_delete_success'), severity: NotificationSeverity.SUCCESS, }); setIsDeleteOpen(false); refetch(); }, onError: () => { showToast({ message: localize('com_ui_share_delete_error'), severity: NotificationSeverity.ERROR, }); }, }); const handleFetchNextPage = useCallback(async () => { if (!hasNextPage || isFetchingNextPage) return; await fetchNextPage(); }, [fetchNextPage, hasNextPage, isFetchingNextPage]); const effectiveIsLoading = isLoading && displayData.length === 0; const effectiveIsFetching = isFetchingNextPage; const confirmDelete = useCallback(() => { if (!deleteRow?.shareId) { showToast({ message: localize('com_ui_share_delete_error'), severity: NotificationSeverity.WARNING, }); return; } deleteMutation.mutate({ shareId: deleteRow.shareId }); }, [deleteMutation, deleteRow, localize, showToast]); const columns: TableColumn, unknown>[] = useMemo( () => [ { accessorKey: 'title', accessorFn: (row: Record): unknown => { const link = row as SharedLinkItem; return link.title; }, header: () => ( {localize('com_ui_name')} ), cell: ({ row }) => { const link = row.original as SharedLinkItem; const { title, shareId } = link; return (
{title}
); }, meta: { className: 'min-w-[150px] flex-1', }, enableSorting: true, }, { accessorKey: 'createdAt', accessorFn: (row: Record): unknown => { const link = row as SharedLinkItem; return link.createdAt; }, header: () => ( {localize('com_ui_date')} ), cell: ({ row }) => { const link = row.original as SharedLinkItem; return formatDate(link.createdAt?.toString() ?? '', isSmallScreen); }, meta: { className: 'w-32 sm:w-40', desktopOnly: true, }, enableSorting: true, }, { id: 'actions', accessorFn: (row: Record): unknown => null, header: () => ( {localize('com_assistants_actions')} ), cell: ({ row }) => { const link = row.original as SharedLinkItem; const { title, conversationId } = link; return (
{ window.open(`/c/${conversationId}`, '_blank'); }} aria-label={localize('com_ui_view_source_conversation', { 0: title })} > } /> { setDeleteRow(link); setIsDeleteOpen(true); }} aria-label={localize('com_ui_delete_link_title', { 0: title })} > } />
); }, meta: { className: 'w-24', }, enableSorting: false, }, ], [isSmallScreen, localize], ); return (
{localize('com_nav_shared_links')}
} selection={{ selectHandler: confirmDelete, selectClasses: `bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white ${ deleteMutation.isLoading ? 'cursor-not-allowed opacity-80' : '' }`, selectText: deleteMutation.isLoading ? : localize('com_ui_delete'), }} /> ); }