mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🎨 style: bookmarks UI update (#3479)
* style: bookmarks update; style(Files): minor update * style: update conversation bookmarks * style: fix w TableCell
This commit is contained in:
parent
3fd25920d4
commit
2d5f704695
15 changed files with 223 additions and 235 deletions
|
|
@ -1,8 +1,7 @@
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { DialogTrigger } from '@radix-ui/react-dialog';
|
|
||||||
import { TConversationTag, TConversation } from 'librechat-data-provider';
|
import { TConversationTag, TConversation } from 'librechat-data-provider';
|
||||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { Dialog, DialogButton } from '~/components/ui/';
|
import { OGDialog, OGDialogTrigger, OGDialogClose } from '~/components/ui/';
|
||||||
import BookmarkForm from './BookmarkForm';
|
import BookmarkForm from './BookmarkForm';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { Spinner } from '../svg';
|
import { Spinner } from '../svg';
|
||||||
|
|
@ -33,9 +32,9 @@ const BookmarkEditDialog = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<OGDialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
<OGDialogTrigger asChild>{trigger}</OGDialogTrigger>
|
||||||
<DialogTemplate
|
<OGDialogTemplate
|
||||||
title="Bookmark"
|
title="Bookmark"
|
||||||
className="w-11/12 sm:w-1/4"
|
className="w-11/12 sm:w-1/4"
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
|
|
@ -51,18 +50,19 @@ const BookmarkEditDialog = ({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
buttons={
|
buttons={
|
||||||
<div className="mb-6 md:mb-2">
|
<OGDialogClose asChild>
|
||||||
<DialogButton
|
<button
|
||||||
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onClick={handleSubmitForm}
|
onClick={handleSubmitForm}
|
||||||
className="bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600"
|
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
||||||
>
|
>
|
||||||
{isLoading ? <Spinner /> : localize('com_ui_save')}
|
{isLoading ? <Spinner /> : localize('com_ui_save')}
|
||||||
</DialogButton>
|
</button>
|
||||||
</div>
|
</OGDialogClose>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</OGDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ const BookmarkForm = ({
|
||||||
aria-invalid={!!errors.tag}
|
aria-invalid={!!errors.tag}
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
'flex h-10 max-h-10 w-full resize-none border-gray-100 px-3 py-2 dark:border-gray-600',
|
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||||
removeFocusOutlines,
|
removeFocusOutlines,
|
||||||
)}
|
)}
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
|
|
@ -158,7 +158,7 @@ const BookmarkForm = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{conversation && (
|
{conversation && (
|
||||||
<div className="flex w-full items-center">
|
<div className="mt-2 flex w-full items-center">
|
||||||
<Controller
|
<Controller
|
||||||
name="addToConversation"
|
name="addToConversation"
|
||||||
control={control}
|
control={control}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const BookmarkItem: FC<MenuItemProps> = ({
|
||||||
<div
|
<div
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
className={cn(
|
className={cn(
|
||||||
'group m-1.5 flex cursor-pointer gap-2 rounded px-1 py-2.5 !pr-3 text-sm !opacity-100 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50',
|
'group m-1.5 flex cursor-pointer gap-2 rounded px-2 py-2.5 !pr-3 text-sm !opacity-100 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50',
|
||||||
'hover:bg-black/5 dark:hover:bg-white/5',
|
'hover:bg-black/5 dark:hover:bg-white/5',
|
||||||
highlightSelected && selected && 'bg-black/5 dark:bg-white/5',
|
highlightSelected && selected && 'bg-black/5 dark:bg-white/5',
|
||||||
)}
|
)}
|
||||||
|
|
@ -58,8 +58,8 @@ const BookmarkItem: FC<MenuItemProps> = ({
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-auto w-9 min-w-max whitespace-nowrap rounded-full bg-white px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-gray-600 ring-1 ring-inset ring-gray-200',
|
'ml-auto w-9 min-w-max whitespace-nowrap rounded-md bg-white px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-gray-600',
|
||||||
'dark:bg-gray-800 dark:text-white dark:ring-gray-100/50',
|
'dark:bg-gray-800 dark:text-white',
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,12 @@ import { TrashIcon } from '~/components/svg';
|
||||||
import { Label } from '~/components/ui';
|
import { Label } from '~/components/ui';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
const DeleteBookmarkButton: FC<{ bookmark: string }> = ({ bookmark }) => {
|
const DeleteBookmarkButton: FC<{
|
||||||
|
bookmark: string;
|
||||||
|
tabIndex?: number;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
}> = ({ bookmark, tabIndex = 0, onFocus, onBlur }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
|
||||||
|
|
@ -37,13 +42,17 @@ const DeleteBookmarkButton: FC<{ bookmark: string }> = ({ bookmark }) => {
|
||||||
title="Delete Bookmark"
|
title="Delete Bookmark"
|
||||||
confirmMessage={
|
confirmMessage={
|
||||||
<Label htmlFor="bookmark" className="text-left text-sm font-medium">
|
<Label htmlFor="bookmark" className="text-left text-sm font-medium">
|
||||||
{localize('com_ui_bookmark_delete_confirm')} : {bookmark}
|
{localize('com_ui_bookmark_delete_confirm')} {bookmark}
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
confirm={confirmDelete}
|
confirm={confirmDelete}
|
||||||
className="hover:text-gray-300 focus-visible:bg-gray-100 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600"
|
className="transition-color flex h-7 w-7 min-w-7 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||||
icon={<TrashIcon className="size-4" />}
|
icon={<TrashIcon className="size-4" />}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteBookmarkButton;
|
export default DeleteBookmarkButton;
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,30 @@ import type { TConversationTag } from 'librechat-data-provider';
|
||||||
import BookmarkEditDialog from './BookmarkEditDialog';
|
import BookmarkEditDialog from './BookmarkEditDialog';
|
||||||
import { EditIcon } from '~/components/svg';
|
import { EditIcon } from '~/components/svg';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui';
|
||||||
const EditBookmarkButton: FC<{ bookmark: TConversationTag }> = ({ bookmark }) => {
|
|
||||||
|
const EditBookmarkButton: FC<{
|
||||||
|
bookmark: TConversationTag;
|
||||||
|
tabIndex?: number;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
}> = ({ bookmark, tabIndex = 0, onFocus, onBlur }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
return (
|
return (
|
||||||
<BookmarkEditDialog
|
<BookmarkEditDialog
|
||||||
bookmark={bookmark}
|
bookmark={bookmark}
|
||||||
trigger={
|
trigger={
|
||||||
<button className="size-4 hover:text-gray-300 focus-visible:bg-gray-100 focus-visible:outline-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 dark:focus-visible:bg-gray-600">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="transition-color flex h-7 w-7 min-w-7 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
>
|
||||||
<TooltipProvider delayDuration={250}>
|
<TooltipProvider delayDuration={250}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<EditIcon />
|
||||||
<EditIcon />
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" sideOffset={0}>
|
<TooltipContent side="top" sideOffset={0}>
|
||||||
{localize('com_ui_edit')}
|
{localize('com_ui_edit')}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export const BookmarkMenuItems: FC<{
|
||||||
trigger={
|
trigger={
|
||||||
<div
|
<div
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
className="group m-1.5 flex cursor-pointer gap-2 rounded px-1 !pr-3.5 pb-2.5 pt-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
|
className="group m-1.5 flex cursor-pointer gap-2 rounded px-2 !pr-3.5 pb-2.5 pt-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<div className="flex grow items-center justify-between gap-2">
|
<div className="flex grow items-center justify-between gap-2">
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
import { BookmarkPlusIcon } from 'lucide-react';
|
import { BookmarkPlusIcon } from 'lucide-react';
|
||||||
import type { FC } from 'react';
|
|
||||||
import { useConversationTagsQuery, useRebuildConversationTagsMutation } from '~/data-provider';
|
import { useConversationTagsQuery, useRebuildConversationTagsMutation } from '~/data-provider';
|
||||||
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
|
import { Button } from '~/components/ui';
|
||||||
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
import { BookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
import { BookmarkEditDialog } from '~/components/Bookmarks';
|
import { BookmarkEditDialog } from '~/components/Bookmarks';
|
||||||
import BookmarkTable from './BookmarkTable';
|
import BookmarkTable from './BookmarkTable';
|
||||||
import { Spinner } from '~/components/svg';
|
import { Spinner } from '~/components/svg';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils/';
|
|
||||||
import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings';
|
import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings';
|
||||||
|
|
||||||
const BookmarkPanel: FC<{ open: boolean; onOpenChange: (open: boolean) => void }> = ({
|
const BookmarkPanel = () => {
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}) => {
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { mutate, isLoading } = useRebuildConversationTagsMutation();
|
const { mutate, isLoading } = useRebuildConversationTagsMutation();
|
||||||
const { data } = useConversationTagsQuery();
|
const { data } = useConversationTagsQuery();
|
||||||
|
|
@ -21,52 +16,31 @@ const BookmarkPanel: FC<{ open: boolean; onOpenChange: (open: boolean) => void }
|
||||||
mutate({});
|
mutate({});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<div className="h-auto max-w-full overflow-x-hidden">
|
||||||
<DialogContent
|
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
|
||||||
showCloseButton={true}
|
<BookmarkTable />
|
||||||
className={cn(
|
<div className="flex justify-between gap-2">
|
||||||
'overflow-x-auto shadow-2xl dark:bg-gray-700 dark:text-white md:max-h-[600px] md:min-h-[373px] md:w-[680px]',
|
<Button variant="outline" onClick={rebuildTags} className="w-50 text-sm">
|
||||||
)}
|
{isLoading ? (
|
||||||
>
|
<Spinner />
|
||||||
<DialogHeader>
|
) : (
|
||||||
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
|
|
||||||
{localize('com_ui_bookmarks')}
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
|
|
||||||
<div className="p-0 sm:p-6 sm:pt-4">
|
|
||||||
<BookmarkTable />
|
|
||||||
<div className="mt-5 sm:mt-4" />
|
|
||||||
<div className="flex justify-between gap-2 pr-2 sm:pr-0">
|
|
||||||
<Button variant="outline" onClick={rebuildTags} className="text-sm">
|
|
||||||
{isLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{localize('com_ui_bookmarks_rebuild')}
|
|
||||||
<HoverCardSettings side="bottom" text="com_nav_info_bookmarks_rebuild" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<BookmarkEditDialog
|
{localize('com_ui_bookmarks_rebuild')}
|
||||||
trigger={
|
<HoverCardSettings side="top" text="com_nav_info_bookmarks_rebuild" />
|
||||||
<Button variant="outline" onClick={rebuildTags} className="text-sm">
|
|
||||||
<BookmarkPlusIcon className="mr-1 size-4" />
|
|
||||||
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button variant="subtle" onClick={() => onOpenChange(!open)} className="text-sm">
|
|
||||||
{localize('com_ui_close')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</Button>
|
||||||
</BookmarkContext.Provider>
|
<BookmarkEditDialog
|
||||||
</DialogContent>
|
trigger={
|
||||||
</Dialog>
|
<Button variant="outline" onClick={rebuildTags} className="w-full text-sm">
|
||||||
|
<BookmarkPlusIcon className="mr-1 size-4" />
|
||||||
|
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BookmarkContext.Provider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default BookmarkPanel;
|
export default BookmarkPanel;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import type { ConversationTagsResponse, TConversationTag } from 'librechat-data-provider';
|
import type { ConversationTagsResponse, TConversationTag } from 'librechat-data-provider';
|
||||||
|
import { Table, TableHeader, TableBody, TableRow, TableCell, Input, Button } from '~/components/ui';
|
||||||
import { BookmarkContext, useBookmarkContext } from '~/Providers/BookmarkContext';
|
import { BookmarkContext, useBookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
import BookmarkTableRow from './BookmarkTableRow';
|
import BookmarkTableRow from './BookmarkTableRow';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
|
||||||
|
|
||||||
const BookmarkTable = () => {
|
const BookmarkTable = () => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const [rows, setRows] = useState<ConversationTagsResponse>([]);
|
const [rows, setRows] = useState<ConversationTagsResponse>([]);
|
||||||
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
const { bookmarks } = useBookmarkContext();
|
const { bookmarks } = useBookmarkContext();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -27,30 +30,65 @@ const BookmarkTable = () => {
|
||||||
return <BookmarkTableRow key={row.tag} moveRow={moveRow} row={row} position={position} />;
|
return <BookmarkTableRow key={row.tag} moveRow={moveRow} row={row} position={position} />;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const filteredRows = rows.filter((row) =>
|
||||||
|
row.tag.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentRows = filteredRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookmarkContext.Provider value={{ bookmarks }}>
|
<BookmarkContext.Provider value={{ bookmarks }}>
|
||||||
<div
|
<div className="flex items-center gap-4 py-4">
|
||||||
className={cn(
|
<Input
|
||||||
'container',
|
placeholder={localize('com_ui_bookmarks_filter')}
|
||||||
'relative h-[300px] overflow-auto',
|
value={searchQuery}
|
||||||
'-mx-4 w-auto ring-1 ring-gray-300 sm:mx-0 sm:rounded-lg',
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
)}
|
className="w-full dark:border-gray-700"
|
||||||
>
|
/>
|
||||||
<table className="min-w-full divide-gray-300">
|
</div>
|
||||||
<thead className="sticky top-0 z-10 border-b bg-white">
|
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">
|
||||||
<tr className="text-left text-sm font-semibold text-gray-900">
|
<Table className="table-fixed border-separate border-spacing-0">
|
||||||
<th className="w-96 px-3 py-3.5 pl-6">
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="w-full px-3 py-3.5 pl-6 dark:bg-gray-700">
|
||||||
<div>{localize('com_ui_bookmarks_title')}</div>
|
<div>{localize('com_ui_bookmarks_title')}</div>
|
||||||
</th>
|
</TableCell>
|
||||||
<th className="w-28 px-3 py-3.5 sm:pl-6">
|
<TableCell className="w-full px-3 py-3.5 dark:bg-gray-700 sm:pl-6">
|
||||||
<div>{localize('com_ui_bookmarks_count')}</div>
|
<div>{localize('com_ui_bookmarks_count')}</div>
|
||||||
</th>
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
<th className="flex-grow px-3 py-3.5 sm:pl-6"> </th>
|
</TableHeader>
|
||||||
</tr>
|
<TableBody>{currentRows.map((row, i) => renderRow(row, i))}</TableBody>
|
||||||
</thead>
|
</Table>
|
||||||
<tbody className="text-sm">{rows.map((row, i) => renderRow(row, i))}</tbody>
|
</div>
|
||||||
</table>
|
<div className="flex items-center justify-between py-4">
|
||||||
|
<div className="pl-1 text-gray-400">
|
||||||
|
{localize('com_ui_showing')} {pageIndex * pageSize + 1} -{' '}
|
||||||
|
{Math.min((pageIndex + 1) * pageSize, filteredRows.length)} {localize('com_ui_of')}{' '}
|
||||||
|
{filteredRows.length}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setPageIndex((prev) => Math.max(prev - 1, 0))}
|
||||||
|
disabled={pageIndex === 0}
|
||||||
|
>
|
||||||
|
{localize('com_ui_prev')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setPageIndex((prev) =>
|
||||||
|
(prev + 1) * pageSize < filteredRows.length ? prev + 1 : prev,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={(pageIndex + 1) * pageSize >= filteredRows.length}
|
||||||
|
>
|
||||||
|
{localize('com_ui_next')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BookmarkContext.Provider>
|
</BookmarkContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,22 @@
|
||||||
import { useRef } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import type { FC } from 'react';
|
|
||||||
import type { Identifier, XYCoord } from 'dnd-core';
|
|
||||||
import type { TConversationTag } from 'librechat-data-provider';
|
import type { TConversationTag } from 'librechat-data-provider';
|
||||||
import { DeleteBookmarkButton, EditBookmarkButton } from '~/components/Bookmarks';
|
import { DeleteBookmarkButton, EditBookmarkButton } from '~/components/Bookmarks';
|
||||||
import { useConversationTagMutation } from '~/data-provider';
|
import { TableRow, TableCell } from '~/components/ui';
|
||||||
import { NotificationSeverity } from '~/common';
|
|
||||||
import { useToastContext } from '~/Providers';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import { cn } from '~/utils';
|
|
||||||
|
|
||||||
export const ItemTypes = {
|
interface BookmarkTableRowProps {
|
||||||
CARD: 'card',
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface BookmarkItemProps {
|
|
||||||
position: number;
|
|
||||||
moveRow: (dragIndex: number, hoverIndex: number) => void;
|
|
||||||
row: TConversationTag;
|
row: TConversationTag;
|
||||||
|
moveRow: (dragIndex: number, hoverIndex: number) => void;
|
||||||
|
position: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DragItem {
|
const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, position }) => {
|
||||||
index: number;
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
id: string;
|
const ref = React.useRef<HTMLTableRowElement>(null);
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BookmarkTableRow: FC<BookmarkItemProps> = ({ position, moveRow, row, ...rest }) => {
|
const [, drop] = useDrop({
|
||||||
const ref = useRef<HTMLTableRowElement>(null);
|
accept: 'bookmark',
|
||||||
|
hover(item: { index: number }) {
|
||||||
const mutation = useConversationTagMutation(row.tag);
|
|
||||||
const localize = useLocalize();
|
|
||||||
const { showToast } = useToastContext();
|
|
||||||
|
|
||||||
const handleDrop = (item: DragItem) => {
|
|
||||||
const data = {
|
|
||||||
...row,
|
|
||||||
position: item.index,
|
|
||||||
};
|
|
||||||
mutation.mutate(data, {
|
|
||||||
onError: () => {
|
|
||||||
showToast({
|
|
||||||
message: localize('com_endpoint_preset_save_error'),
|
|
||||||
severity: NotificationSeverity.ERROR,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
|
|
||||||
accept: ItemTypes.CARD,
|
|
||||||
collect(monitor) {
|
|
||||||
return {
|
|
||||||
handlerId: monitor.getHandlerId(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
drop(item: DragItem, monitor) {
|
|
||||||
handleDrop(item);
|
|
||||||
},
|
|
||||||
hover(item: DragItem, monitor) {
|
|
||||||
if (!ref.current) {
|
if (!ref.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -67,68 +25,60 @@ const BookmarkTableRow: FC<BookmarkItemProps> = ({ position, moveRow, row, ...re
|
||||||
if (dragIndex === hoverIndex) {
|
if (dragIndex === hoverIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
|
||||||
|
|
||||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
|
||||||
|
|
||||||
const clientOffset = monitor.getClientOffset();
|
|
||||||
|
|
||||||
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
|
|
||||||
|
|
||||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
moveRow(dragIndex, hoverIndex);
|
moveRow(dragIndex, hoverIndex);
|
||||||
|
|
||||||
item.index = hoverIndex;
|
item.index = hoverIndex;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [{ isDragging }, drag] = useDrag({
|
const [{ isDragging }, drag] = useDrag({
|
||||||
type: ItemTypes.CARD,
|
type: 'bookmark',
|
||||||
item: () => {
|
item: { index: position },
|
||||||
return { id: row.tag, index: position };
|
|
||||||
},
|
|
||||||
collect: (monitor) => ({
|
collect: (monitor) => ({
|
||||||
isDragging: monitor.isDragging(),
|
isDragging: monitor.isDragging(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (position > 0) {
|
drag(drop(ref));
|
||||||
drag(drop(ref));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<TableRow
|
||||||
className={cn(
|
|
||||||
'group cursor-pointer gap-2 rounded text-sm hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5',
|
|
||||||
isDragging ? 'opacity-0' : 'opacity-100',
|
|
||||||
)}
|
|
||||||
key={row.tag}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-handler-id={handlerId}
|
className="cursor-move hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||||
role="menuitem"
|
style={{ opacity: isDragging ? 0.5 : 1 }}
|
||||||
tabIndex={-1}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
{...rest}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
<td className="w-96 py-2 pl-6 pr-3">{row.tag}</td>
|
<TableCell className="w-full px-3 py-3.5 pl-6">
|
||||||
<td className={cn('w-28 py-2 pl-4 pr-3 sm:pl-6')}>
|
<div className="truncate">{row.tag}</div>
|
||||||
<span className="py-1">{row.count}</span>
|
</TableCell>
|
||||||
</td>
|
<TableCell className="w-full px-3 py-3.5 sm:pl-6">
|
||||||
<td className="flex-grow py-2 pl-4 pr-4 sm:pl-6">
|
<div className="flex items-center justify-between py-1">
|
||||||
{position > 0 && (
|
<div>{row.count}</div>
|
||||||
<div className="flex w-full items-center justify-end gap-2 py-1 text-gray-400">
|
<div
|
||||||
<EditBookmarkButton bookmark={row} />
|
className="flex items-center gap-2"
|
||||||
<DeleteBookmarkButton bookmark={row.tag} />
|
style={{
|
||||||
|
opacity: isHovered ? 1 : 0,
|
||||||
|
transition: 'opacity 0.1s ease-in-out',
|
||||||
|
}}
|
||||||
|
onFocus={() => setIsHovered(true)}
|
||||||
|
onBlur={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<EditBookmarkButton
|
||||||
|
bookmark={row}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() => setIsHovered(true)}
|
||||||
|
onBlur={() => setIsHovered(false)}
|
||||||
|
/>
|
||||||
|
<DeleteBookmarkButton
|
||||||
|
bookmark={row.tag}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() => setIsHovered(true)}
|
||||||
|
onBlur={() => setIsHovered(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</td>
|
</TableCell>
|
||||||
</tr>
|
</TableRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,6 @@ export default function AssistantTool({
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
title={localize('com_ui_delete_tool')}
|
title={localize('com_ui_delete_tool')}
|
||||||
mainClassName="px-0"
|
|
||||||
className="max-w-[450px]"
|
className="max-w-[450px]"
|
||||||
main={
|
main={
|
||||||
<Label className="text-left text-sm font-medium">
|
<Label className="text-left text-sm font-medium">
|
||||||
|
|
|
||||||
|
|
@ -72,12 +72,12 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-4 px-2 py-4">
|
<div className="flex items-center gap-4 py-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder={localize('com_files_filter')}
|
placeholder={localize('com_files_filter')}
|
||||||
value={(table.getColumn('filename')?.getFilterValue() as string) ?? ''}
|
value={(table.getColumn('filename')?.getFilterValue() as string) ?? ''}
|
||||||
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
|
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
|
||||||
className="max-w-xs dark:border-gray-700"
|
className="w-full dark:border-gray-700"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">
|
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">
|
||||||
|
|
@ -90,7 +90,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
||||||
<TableHead
|
<TableHead
|
||||||
key={header.id}
|
key={header.id}
|
||||||
style={{ width: index === 0 ? '75%' : '25%' }}
|
style={{ width: index === 0 ? '75%' : '25%' }}
|
||||||
className="sticky top-0 h-auto border-b border-black/10 bg-white py-1 text-left font-medium text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-100"
|
className="sticky top-0 h-auto bg-white py-1 text-left font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-100"
|
||||||
>
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
|
|
@ -133,17 +133,19 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-around space-x-2 py-4">
|
<div className="flex items-center justify-between py-4">
|
||||||
<Button
|
<div className="flex items-center">
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => setShowFiles(true)}
|
size="sm"
|
||||||
className="flex gap-2"
|
onClick={() => setShowFiles(true)}
|
||||||
>
|
className="flex gap-2"
|
||||||
<LucideArrowUpLeft className="icon-sm" />
|
>
|
||||||
{localize('com_sidepanel_manage_files')}
|
<LucideArrowUpLeft className="icon-sm" />
|
||||||
</Button>
|
{localize('com_sidepanel_manage_files')}
|
||||||
<div className="flex gap-2">
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
||||||
<OGDialogTitle>{title}</OGDialogTitle>
|
<OGDialogTitle>{title}</OGDialogTitle>
|
||||||
{description && <OGDialogDescription className="">{description}</OGDialogDescription>}
|
{description && <OGDialogDescription className="">{description}</OGDialogDescription>}
|
||||||
</OGDialogHeader>
|
</OGDialogHeader>
|
||||||
<div className={cn('px-6', mainClassName)}>{main ? main : null}</div>
|
<div className={cn('px-0', mainClassName)}>{main ? main : null}</div>
|
||||||
<OGDialogFooter className={footerClassName}>
|
<OGDialogFooter className={footerClassName}>
|
||||||
<div>{leftButtons ? leftButtons : null}</div>
|
<div>{leftButtons ? leftButtons : null}</div>
|
||||||
<div className="flex h-auto gap-3">
|
<div className="flex h-auto gap-3">
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
OGDialog,
|
||||||
DialogTrigger,
|
OGDialogTrigger,
|
||||||
Label,
|
Label,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '~/components/ui';
|
} from '~/components/ui';
|
||||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { CrossIcon } from '~/components/svg';
|
import { CrossIcon } from '~/components/svg';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
|
@ -20,6 +20,9 @@ export default function TooltipIcon({
|
||||||
confirm,
|
confirm,
|
||||||
confirmMessage,
|
confirmMessage,
|
||||||
icon,
|
icon,
|
||||||
|
tabIndex,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
}: {
|
}: {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -28,6 +31,9 @@ export default function TooltipIcon({
|
||||||
confirm?: () => void;
|
confirm?: () => void;
|
||||||
confirmMessage?: ReactElement;
|
confirmMessage?: ReactElement;
|
||||||
icon?: ReactElement;
|
icon?: ReactElement;
|
||||||
|
tabIndex?: number;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
|
@ -55,27 +61,29 @@ export default function TooltipIcon({
|
||||||
|
|
||||||
if (!confirmMessage) {
|
if (!confirmMessage) {
|
||||||
return (
|
return (
|
||||||
<button className={className} onClick={confirm}>
|
<button
|
||||||
|
className={className}
|
||||||
|
onClick={confirm}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
>
|
||||||
{disabled ? <CrossIcon /> : renderDeleteButton()}
|
{disabled ? <CrossIcon /> : renderDeleteButton()}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<OGDialog>
|
||||||
<DialogTrigger asChild>
|
<OGDialogTrigger asChild>
|
||||||
<button className={className}>{disabled ? <CrossIcon /> : renderDeleteButton()}</button>
|
<button className={className} tabIndex={tabIndex} onFocus={onFocus} onBlur={onBlur}>
|
||||||
</DialogTrigger>
|
{disabled ? <CrossIcon /> : renderDeleteButton()}
|
||||||
<DialogTemplate
|
</button>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
<OGDialogTemplate
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
title={title}
|
title={title}
|
||||||
className="max-w-[450px]"
|
className="max-w-[450px]"
|
||||||
main={
|
main={<Label className="text-left text-sm font-medium">{confirmMessage}</Label>}
|
||||||
<>
|
|
||||||
<div className="flex w-full flex-col items-center gap-2">
|
|
||||||
<div className="grid w-full items-center gap-2">{confirmMessage}</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
selection={{
|
selection={{
|
||||||
selectHandler: confirm,
|
selectHandler: confirm,
|
||||||
selectClasses:
|
selectClasses:
|
||||||
|
|
@ -83,6 +91,6 @@ export default function TooltipIcon({
|
||||||
selectText: localize('com_ui_delete'),
|
selectText: localize('com_ui_delete'),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</OGDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
|
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
|
||||||
import type { NavLink } from '~/common';
|
import type { NavLink } from '~/common';
|
||||||
|
import BookmarkPanel from '~/components/SidePanel/Bookmarks/BookmarkPanel';
|
||||||
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
|
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
|
||||||
import PromptsAccordion from '~/components/Prompts/PromptsAccordion';
|
import PromptsAccordion from '~/components/Prompts/PromptsAccordion';
|
||||||
// import Parameters from '~/components/SidePanel/Parameters/Panel';
|
// import Parameters from '~/components/SidePanel/Parameters/Panel';
|
||||||
|
|
@ -26,14 +27,12 @@ export default function useSideNavLinks({
|
||||||
keyProvided,
|
keyProvided,
|
||||||
endpoint,
|
endpoint,
|
||||||
interfaceConfig,
|
interfaceConfig,
|
||||||
manageBookmarks,
|
|
||||||
}: {
|
}: {
|
||||||
hidePanel: () => void;
|
hidePanel: () => void;
|
||||||
assistants?: TConfig | null;
|
assistants?: TConfig | null;
|
||||||
keyProvided: boolean;
|
keyProvided: boolean;
|
||||||
endpoint?: EModelEndpoint | null;
|
endpoint?: EModelEndpoint | null;
|
||||||
interfaceConfig: Partial<TInterfaceConfig>;
|
interfaceConfig: Partial<TInterfaceConfig>;
|
||||||
manageBookmarks: (e?: React.MouseEvent) => void;
|
|
||||||
}) {
|
}) {
|
||||||
const hasAccessToPrompts = useHasAccess({
|
const hasAccessToPrompts = useHasAccess({
|
||||||
permissionType: PermissionTypes.PROMPTS,
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
|
|
@ -80,8 +79,8 @@ export default function useSideNavLinks({
|
||||||
title: 'com_sidepanel_conversation_tags',
|
title: 'com_sidepanel_conversation_tags',
|
||||||
label: '',
|
label: '',
|
||||||
icon: Bookmark,
|
icon: Bookmark,
|
||||||
onClick: manageBookmarks,
|
|
||||||
id: 'bookmarks',
|
id: 'bookmarks',
|
||||||
|
Component: BookmarkPanel,
|
||||||
});
|
});
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
|
|
@ -100,7 +99,6 @@ export default function useSideNavLinks({
|
||||||
endpoint,
|
endpoint,
|
||||||
interfaceConfig.parameters,
|
interfaceConfig.parameters,
|
||||||
hasAccessToPrompts,
|
hasAccessToPrompts,
|
||||||
manageBookmarks,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return Links;
|
return Links;
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,7 @@ export default {
|
||||||
com_ui_bookmarks_update_error: 'There was an error updating the bookmark',
|
com_ui_bookmarks_update_error: 'There was an error updating the bookmark',
|
||||||
com_ui_bookmarks_delete_error: 'There was an error deleting the bookmark',
|
com_ui_bookmarks_delete_error: 'There was an error deleting the bookmark',
|
||||||
com_ui_bookmarks_add_to_conversation: 'Add to current conversation',
|
com_ui_bookmarks_add_to_conversation: 'Add to current conversation',
|
||||||
|
com_ui_bookmarks_filter: 'Filter bookmarks...',
|
||||||
com_auth_error_login:
|
com_auth_error_login:
|
||||||
'Unable to login with the information provided. Please check your credentials and try again.',
|
'Unable to login with the information provided. Please check your credentials and try again.',
|
||||||
com_auth_error_login_rl:
|
com_auth_error_login_rl:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue