| import React, { useCallback, useEffect, useState } from 'react'; |
| import { BookmarkPlusIcon } from 'lucide-react'; |
| import { |
| Table, |
| Input, |
| Button, |
| TableRow, |
| TableHead, |
| TableBody, |
| TableCell, |
| TableHeader, |
| OGDialogTrigger, |
| } from '@librechat/client'; |
| import type { ConversationTagsResponse, TConversationTag } from 'librechat-data-provider'; |
| import { BookmarkContext, useBookmarkContext } from '~/Providers/BookmarkContext'; |
| import { BookmarkEditDialog } from '~/components/Bookmarks'; |
| import BookmarkTableRow from './BookmarkTableRow'; |
| import { useLocalize } from '~/hooks'; |
|
|
| const removeDuplicates = (bookmarks: TConversationTag[]) => { |
| const seen = new Set(); |
| return bookmarks.filter((bookmark) => { |
| const duplicate = seen.has(bookmark._id); |
| seen.add(bookmark._id); |
| return !duplicate; |
| }); |
| }; |
|
|
| const BookmarkTable = () => { |
| const localize = useLocalize(); |
| const [rows, setRows] = useState<ConversationTagsResponse>([]); |
| const [pageIndex, setPageIndex] = useState(0); |
| const [searchQuery, setSearchQuery] = useState(''); |
| const [open, setOpen] = useState(false); |
| const pageSize = 10; |
|
|
| const { bookmarks = [] } = useBookmarkContext(); |
|
|
| useEffect(() => { |
| const _bookmarks = removeDuplicates(bookmarks).sort((a, b) => a.position - b.position); |
| setRows(_bookmarks); |
| }, [bookmarks]); |
|
|
| const moveRow = useCallback((dragIndex: number, hoverIndex: number) => { |
| setRows((prevTags: TConversationTag[]) => { |
| const updatedRows = [...prevTags]; |
| const [movedRow] = updatedRows.splice(dragIndex, 1); |
| updatedRows.splice(hoverIndex, 0, movedRow); |
| return updatedRows.map((row, index) => ({ ...row, position: index })); |
| }); |
| }, []); |
|
|
| const renderRow = useCallback( |
| (row: TConversationTag) => ( |
| <BookmarkTableRow key={row._id} moveRow={moveRow} row={row} position={row.position} /> |
| ), |
| [moveRow], |
| ); |
|
|
| const filteredRows = rows.filter( |
| (row) => row.tag && row.tag.toLowerCase().includes(searchQuery.toLowerCase()), |
| ); |
|
|
| const currentRows = filteredRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); |
|
|
| return ( |
| <BookmarkContext.Provider value={{ bookmarks }}> |
| <div role="region" aria-label={localize('com_ui_bookmarks')} className="mt-2 space-y-2"> |
| <div className="flex items-center gap-4"> |
| <Input |
| placeholder={localize('com_ui_bookmarks_filter')} |
| value={searchQuery} |
| onChange={(e) => setSearchQuery(e.target.value)} |
| aria-label={localize('com_ui_bookmarks_filter')} |
| /> |
| </div> |
| |
| <div className="rounded-lg border border-border-light bg-transparent shadow-sm transition-colors"> |
| <Table className="w-full table-fixed"> |
| <TableHeader> |
| <TableRow className="border-b border-border-light"> |
| <TableHead className="w-[70%] bg-surface-secondary py-3 text-left text-sm font-medium text-text-secondary"> |
| <div>{localize('com_ui_bookmarks_title')}</div> |
| </TableHead> |
| <TableHead className="w-[30%] bg-surface-secondary py-3 text-left text-sm font-medium text-text-secondary"> |
| <div>{localize('com_ui_bookmarks_count')}</div> |
| </TableHead> |
| <TableHead className="w-[40%] bg-surface-secondary py-3 text-left text-sm font-medium text-text-secondary"> |
| <div>{localize('com_assistants_actions')}</div> |
| </TableHead> |
| </TableRow> |
| </TableHeader> |
| <TableBody> |
| {currentRows.length ? ( |
| currentRows.map(renderRow) |
| ) : ( |
| <TableRow> |
| <TableCell colSpan={3} className="h-24 text-center text-sm text-text-secondary"> |
| {localize('com_ui_no_bookmarks')} |
| </TableCell> |
| </TableRow> |
| )} |
| </TableBody> |
| </Table> |
| </div> |
| |
| <div className="flex items-center justify-between"> |
| <div className="flex justify-between gap-2"> |
| <BookmarkEditDialog context="BookmarkPanel" open={open} setOpen={setOpen}> |
| <OGDialogTrigger asChild> |
| <Button |
| variant="outline" |
| size="sm" |
| className="w-full gap-2 text-sm" |
| aria-label={localize('com_ui_bookmarks_new')} |
| onClick={() => setOpen(!open)} |
| > |
| <BookmarkPlusIcon className="size-4" /> |
| <div className="break-all">{localize('com_ui_bookmarks_new')}</div> |
| </Button> |
| </OGDialogTrigger> |
| </BookmarkEditDialog> |
| </div> |
| <div className="flex items-center gap-2" role="navigation" aria-label="Pagination"> |
| <Button |
| variant="outline" |
| size="sm" |
| onClick={() => setPageIndex((prev) => Math.max(prev - 1, 0))} |
| disabled={pageIndex === 0} |
| aria-label={localize('com_ui_prev')} |
| > |
| {localize('com_ui_prev')} |
| </Button> |
| <div aria-live="polite" className="text-sm"> |
| {`${pageIndex + 1} / ${Math.ceil(filteredRows.length / pageSize)}`} |
| </div> |
| <Button |
| variant="outline" |
| size="sm" |
| onClick={() => |
| setPageIndex((prev) => |
| (prev + 1) * pageSize < filteredRows.length ? prev + 1 : prev, |
| ) |
| } |
| disabled={(pageIndex + 1) * pageSize >= filteredRows.length} |
| aria-label={localize('com_ui_next')} |
| > |
| {localize('com_ui_next')} |
| </Button> |
| </div> |
| </div> |
| </div> |
| </BookmarkContext.Provider> |
| ); |
| }; |
|
|
| export default BookmarkTable; |
|
|