mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
Refactor Chat Input File Table Headers to Use SortFilterHeader Component
- Replaced button-based sorting headers in the Chat Input Files Table with a new SortFilterHeader component for better code organization and consistency. - Updated the header for filename, updatedAt, and bytes columns to utilize the new component. Enhance Navigation Component with Skeleton Loading States - Added Skeleton loading states to the Nav component for better user experience during data fetching. - Updated Suspense fallbacks for AgentMarketplaceButton and BookmarkNav components to display Skeletons. Refactor Avatar Component for Improved UI - Enhanced the Avatar component by adding a Label for drag-and-drop functionality. - Improved styling and structure for the file upload area. Update Shared Links Component for Better Error Handling and Sorting - Improved error handling in the Shared Links component for fetching next pages and deleting shared links. - Simplified the header rendering for sorting columns and added sorting functionality to the title and createdAt columns. Refactor Archived Chats Component - Merged ArchivedChats and ArchivedChatsTable components into a single ArchivedChats component for better maintainability. - Implemented sorting and searching functionality with debouncing for improved performance. - Enhanced the UI with better loading states and error handling. Update DataTable Component for Sorting Icons - Added sorting icons (ChevronUp, ChevronDown, ChevronsUpDown) to the DataTable headers for better visual feedback on sorting state. Localization Updates - Updated translation.json to fix missing translations and improve existing ones for better user experience.
This commit is contained in:
parent
ae74ede48a
commit
7252b83235
9 changed files with 428 additions and 461 deletions
|
@ -61,16 +61,7 @@ export const columns: ColumnDef<TFile>[] = [
|
||||||
accessorKey: 'filename',
|
accessorKey: 'filename',
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
return (
|
return <SortFilterHeader column={column} title={localize('com_ui_name')} />;
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
||||||
>
|
|
||||||
{localize('com_ui_name')}
|
|
||||||
<ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const file = row.original;
|
const file = row.original;
|
||||||
|
@ -100,16 +91,7 @@ export const columns: ColumnDef<TFile>[] = [
|
||||||
accessorKey: 'updatedAt',
|
accessorKey: 'updatedAt',
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
return (
|
return <SortFilterHeader column={column} title={localize('com_ui_date')} />;
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
||||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
|
||||||
>
|
|
||||||
{localize('com_ui_date')}
|
|
||||||
<ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
|
@ -197,16 +179,7 @@ export const columns: ColumnDef<TFile>[] = [
|
||||||
accessorKey: 'bytes',
|
accessorKey: 'bytes',
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
return (
|
return <SortFilterHeader column={column} title={localize('com_ui_size')} />;
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
||||||
>
|
|
||||||
{localize('com_ui_size')}
|
|
||||||
<ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const suffix = ' MB';
|
const suffix = ' MB';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react';
|
import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useMediaQuery } from '@librechat/client';
|
import { Skeleton, useMediaQuery } from '@librechat/client';
|
||||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import type { ConversationListResponse } from 'librechat-data-provider';
|
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||||
import type { InfiniteQueryObserverResult } from '@tanstack/react-query';
|
import type { InfiniteQueryObserverResult } from '@tanstack/react-query';
|
||||||
|
@ -158,13 +158,12 @@ const Nav = memo(
|
||||||
const headerButtons = useMemo(
|
const headerButtons = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<>
|
<>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={<Skeleton className="h-10 w-10 rounded-xl" />}>
|
||||||
<AgentMarketplaceButton isSmallScreen={isSmallScreen} toggleNav={toggleNavVisible} />
|
<AgentMarketplaceButton isSmallScreen={isSmallScreen} toggleNav={toggleNavVisible} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{hasAccessToBookmarks && (
|
{hasAccessToBookmarks && (
|
||||||
<>
|
<>
|
||||||
<div className="mt-1.5" />
|
<Suspense fallback={<Skeleton className="h-10 w-10 rounded-xl" />}>
|
||||||
<Suspense fallback={null}>
|
|
||||||
<BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} />
|
<BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
|
@ -229,7 +228,7 @@ const Nav = memo(
|
||||||
isSearchLoading={isSearchLoading}
|
isSearchLoading={isSearchLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={<Skeleton className="mt-1 h-12 w-full rounded-xl" />}>
|
||||||
<AccountSettings />
|
<AccountSettings />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import AvatarEditor from 'react-avatar-editor';
|
||||||
import { FileImage, RotateCw, Upload } from 'lucide-react';
|
import { FileImage, RotateCw, Upload } from 'lucide-react';
|
||||||
import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
|
import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
|
Label,
|
||||||
Slider,
|
Slider,
|
||||||
Button,
|
Button,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
@ -199,25 +200,27 @@ function Avatar() {
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="flex w-full flex-col items-center space-y-4">
|
||||||
className="flex h-64 w-11/12 flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-transparent dark:border-gray-600"
|
<div
|
||||||
onDrop={handleDrop}
|
className="flex h-64 w-11/12 flex-col items-center justify-center rounded-lg border-2 border-dashed border-border-light bg-transparent"
|
||||||
onDragOver={handleDragOver}
|
onDrop={handleDrop}
|
||||||
>
|
onDragOver={handleDragOver}
|
||||||
<FileImage className="mb-4 size-12 text-gray-400" />
|
>
|
||||||
<p className="mb-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
<FileImage className="mb-4 size-12 text-text-tertiary" />
|
||||||
{localize('com_ui_drag_drop')}
|
<Label className="mb-2 px-2 text-center text-sm text-text-tertiary">
|
||||||
</p>
|
{localize('com_ui_drag_drop_image')}
|
||||||
<Button variant="secondary" onClick={openFileDialog}>
|
</Label>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
accept=".png, .jpg, .jpeg"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button variant="secondary" onClick={openFileDialog} className="w-11/12">
|
||||||
{localize('com_ui_select_file')}
|
{localize('com_ui_select_file')}
|
||||||
</Button>
|
</Button>
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
className="hidden"
|
|
||||||
accept=".png, .jpg, .jpeg"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useCallback, useState, useMemo, useEffect } from 'react';
|
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { TrashIcon, MessageSquare, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
import { TrashIcon, MessageSquare } from 'lucide-react';
|
||||||
import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider';
|
import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
OGDialog,
|
OGDialog,
|
||||||
|
@ -20,15 +20,13 @@ import {
|
||||||
Label,
|
Label,
|
||||||
} from '@librechat/client';
|
} from '@librechat/client';
|
||||||
import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provider';
|
import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provider';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import { NotificationSeverity } from '~/common';
|
import { NotificationSeverity } from '~/common';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
import { formatDate } from '~/utils';
|
import { formatDate } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
|
||||||
|
|
||||||
const DEFAULT_PARAMS: SharedLinksListParams = {
|
const DEFAULT_PARAMS: SharedLinksListParams = {
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: 25,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
sortBy: 'createdAt',
|
sortBy: 'createdAt',
|
||||||
sortDirection: 'desc',
|
sortDirection: 'desc',
|
||||||
|
@ -44,16 +42,33 @@ export default function SharedLinks() {
|
||||||
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
|
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
|
||||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const prevSortRef = useRef({
|
||||||
|
sortBy: DEFAULT_PARAMS.sortBy,
|
||||||
|
sortDirection: DEFAULT_PARAMS.sortDirection,
|
||||||
|
});
|
||||||
|
|
||||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } =
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } =
|
||||||
useSharedLinksQuery(queryParams, {
|
useSharedLinksQuery(queryParams, {
|
||||||
enabled: isOpen,
|
enabled: isOpen,
|
||||||
staleTime: 0,
|
staleTime: 30 * 1000,
|
||||||
cacheTime: 5 * 60 * 1000,
|
cacheTime: 5 * 60 * 1000,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
|
keepPreviousData: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const { sortBy, sortDirection } = queryParams;
|
||||||
|
const prevSort = prevSortRef.current;
|
||||||
|
|
||||||
|
if (sortBy !== prevSort.sortBy || sortDirection !== prevSort.sortDirection) {
|
||||||
|
refetch();
|
||||||
|
prevSortRef.current = { sortBy, sortDirection };
|
||||||
|
}
|
||||||
|
}, [queryParams, isOpen, refetch]);
|
||||||
|
|
||||||
const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => {
|
const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => {
|
||||||
setQueryParams((prev) => ({
|
setQueryParams((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
@ -71,7 +86,7 @@ export default function SharedLinks() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const debouncedFilterChange = useMemo(
|
const debouncedFilterChange = useMemo(
|
||||||
() => debounce(handleFilterChange, 300),
|
() => debounce(handleFilterChange, 500), // Increased debounce time to 500ms
|
||||||
[handleFilterChange],
|
[handleFilterChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -134,7 +149,7 @@ export default function SharedLinks() {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete shared links:', error);
|
console.error('Failed to delete shared links:', error);
|
||||||
showToast({
|
showToast({
|
||||||
message: localize('com_ui_bulk_delete_error'),
|
message: localize('com_ui_share_delete_error'),
|
||||||
severity: NotificationSeverity.ERROR,
|
severity: NotificationSeverity.ERROR,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -146,8 +161,17 @@ export default function SharedLinks() {
|
||||||
if (hasNextPage !== true || isFetchingNextPage) {
|
if (hasNextPage !== true || isFetchingNextPage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await fetchNextPage();
|
|
||||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
try {
|
||||||
|
await fetchNextPage();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch next page:', error);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_share_delete_error'),
|
||||||
|
severity: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [fetchNextPage, hasNextPage, isFetchingNextPage, showToast, localize]);
|
||||||
|
|
||||||
const confirmDelete = useCallback(() => {
|
const confirmDelete = useCallback(() => {
|
||||||
if (deleteRow) {
|
if (deleteRow) {
|
||||||
|
@ -160,28 +184,7 @@ export default function SharedLinks() {
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
accessorKey: 'title',
|
accessorKey: 'title',
|
||||||
header: () => {
|
header: () => <span className="text-xs sm:text-sm">{localize('com_ui_name')}</span>,
|
||||||
const isSorted = queryParams.sortBy === 'title';
|
|
||||||
const sortDirection = queryParams.sortDirection;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleSort('title', isSorted && sortDirection === 'asc' ? 'desc' : 'asc')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{localize('com_ui_name')}
|
|
||||||
{isSorted && sortDirection === 'asc' && (
|
|
||||||
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
)}
|
|
||||||
{isSorted && sortDirection === 'desc' && (
|
|
||||||
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
)}
|
|
||||||
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const { title, shareId } = row.original;
|
const { title, shareId } = row.original;
|
||||||
return (
|
return (
|
||||||
|
@ -201,36 +204,17 @@ export default function SharedLinks() {
|
||||||
meta: {
|
meta: {
|
||||||
size: '35%',
|
size: '35%',
|
||||||
mobileSize: '50%',
|
mobileSize: '50%',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
header: () => {
|
header: () => <span className="text-xs sm:text-sm">{localize('com_ui_date')}</span>,
|
||||||
const isSorted = queryParams.sortBy === 'createdAt';
|
|
||||||
const sortDirection = queryParams.sortDirection;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleSort('createdAt', isSorted && sortDirection === 'asc' ? 'desc' : 'asc')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{localize('com_ui_date')}
|
|
||||||
{isSorted && sortDirection === 'asc' && (
|
|
||||||
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
)}
|
|
||||||
{isSorted && sortDirection === 'desc' && (
|
|
||||||
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
)}
|
|
||||||
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen),
|
cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen),
|
||||||
meta: {
|
meta: {
|
||||||
size: '10%',
|
size: '10%',
|
||||||
mobileSize: '20%',
|
mobileSize: '20%',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -243,6 +227,7 @@ export default function SharedLinks() {
|
||||||
meta: {
|
meta: {
|
||||||
size: '7%',
|
size: '7%',
|
||||||
mobileSize: '25%',
|
mobileSize: '25%',
|
||||||
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -281,22 +266,17 @@ export default function SharedLinks() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[isSmallScreen, localize, queryParams, handleSort],
|
[isSmallScreen, localize],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>{localize('com_nav_shared_links')}</div>
|
<div>{localize('com_nav_shared_links')}</div>
|
||||||
|
|
||||||
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
|
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<OGDialogTrigger asChild onClick={() => setIsOpen(true)}>
|
<OGDialogTrigger asChild onClick={() => setIsOpen(true)}>
|
||||||
<Button variant="outline">{localize('com_ui_manage')}</Button>
|
<Button variant="outline">{localize('com_ui_manage')}</Button>
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
|
<OGDialogContent className="w-11/12 max-w-5xl">
|
||||||
<OGDialogContent
|
|
||||||
title={localize('com_nav_my_files')}
|
|
||||||
className="w-11/12 max-w-5xl bg-background text-text-primary shadow-2xl"
|
|
||||||
>
|
|
||||||
<OGDialogHeader>
|
<OGDialogHeader>
|
||||||
<OGDialogTitle>{localize('com_nav_shared_links')}</OGDialogTitle>
|
<OGDialogTitle>{localize('com_nav_shared_links')}</OGDialogTitle>
|
||||||
</OGDialogHeader>
|
</OGDialogHeader>
|
||||||
|
@ -312,7 +292,10 @@ export default function SharedLinks() {
|
||||||
onFilterChange={debouncedFilterChange}
|
onFilterChange={debouncedFilterChange}
|
||||||
filterValue={queryParams.search}
|
filterValue={queryParams.search}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
enableSearch={isSearchEnabled}
|
enableSearch={!!isSearchEnabled}
|
||||||
|
onSortChange={handleSort}
|
||||||
|
sortBy={queryParams.sortBy}
|
||||||
|
sortDirection={queryParams.sortDirection}
|
||||||
/>
|
/>
|
||||||
</OGDialogContent>
|
</OGDialogContent>
|
||||||
</OGDialog>
|
</OGDialog>
|
||||||
|
@ -320,7 +303,7 @@ export default function SharedLinks() {
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
title={localize('com_ui_delete_shared_link')}
|
title={localize('com_ui_delete_shared_link')}
|
||||||
className="max-w-[450px]"
|
className="w-11/12 max-w-md"
|
||||||
main={
|
main={
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col items-center gap-2">
|
<div className="flex w-full flex-col items-center gap-2">
|
||||||
|
|
|
@ -1,11 +1,275 @@
|
||||||
import { useState } from 'react';
|
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||||
import { OGDialogTemplate, OGDialog, OGDialogTrigger, Button } from '@librechat/client';
|
import debounce from 'lodash/debounce';
|
||||||
import ArchivedChatsTable from './ArchivedChatsTable';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { TrashIcon, ArchiveRestore } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
OGDialog,
|
||||||
|
OGDialogTrigger,
|
||||||
|
OGDialogTemplate,
|
||||||
|
OGDialogContent,
|
||||||
|
OGDialogHeader,
|
||||||
|
OGDialogTitle,
|
||||||
|
Label,
|
||||||
|
TooltipAnchor,
|
||||||
|
Spinner,
|
||||||
|
DataTable,
|
||||||
|
useToastContext,
|
||||||
|
useMediaQuery,
|
||||||
|
} from '@librechat/client';
|
||||||
|
import type { ConversationListParams, TConversation } from 'librechat-data-provider';
|
||||||
|
import {
|
||||||
|
useArchiveConvoMutation,
|
||||||
|
useConversationsInfiniteQuery,
|
||||||
|
useDeleteConversationMutation,
|
||||||
|
} from '~/data-provider';
|
||||||
|
import { MinimalIcon } from '~/components/Endpoints';
|
||||||
|
import { NotificationSeverity } from '~/common';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { formatDate } from '~/utils';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
export default function ArchivedChats() {
|
const DEFAULT_PARAMS: ConversationListParams = {
|
||||||
|
isArchived: true,
|
||||||
|
sortBy: 'createdAt',
|
||||||
|
sortDirection: 'desc',
|
||||||
|
search: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortField = 'title' | 'createdAt';
|
||||||
|
|
||||||
|
export default function ArchivedChatsTable() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const isSearchEnabled = useRecoilValue(store.search);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||||
|
const [deleteRow, setDeleteRow] = useState<TConversation | null>(null);
|
||||||
|
const [unarchivingId, setUnarchivingId] = useState<string | null>(null);
|
||||||
|
const prevSortRef = useRef({
|
||||||
|
sortBy: DEFAULT_PARAMS.sortBy,
|
||||||
|
sortDirection: DEFAULT_PARAMS.sortDirection,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [queryParams, setQueryParams] = useState<ConversationListParams>(DEFAULT_PARAMS);
|
||||||
|
const [searchInput, setSearchInput] = useState('');
|
||||||
|
|
||||||
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } =
|
||||||
|
useConversationsInfiniteQuery(queryParams, {
|
||||||
|
enabled: isOpen,
|
||||||
|
staleTime: 30 * 1000,
|
||||||
|
cacheTime: 5 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
keepPreviousData: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSort = useCallback((field: string, direction: 'asc' | 'desc') => {
|
||||||
|
setQueryParams((prev) => ({
|
||||||
|
...prev,
|
||||||
|
sortBy: field as SortField,
|
||||||
|
sortDirection: direction,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Trigger refetch when sort parameters change
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return; // Only refetch if dialog is open
|
||||||
|
|
||||||
|
const { sortBy, sortDirection } = queryParams;
|
||||||
|
const prevSort = prevSortRef.current;
|
||||||
|
|
||||||
|
if (sortBy !== prevSort.sortBy || sortDirection !== prevSort.sortDirection) {
|
||||||
|
console.log('Sort changed, refetching...', { from: prevSort, to: { sortBy, sortDirection } });
|
||||||
|
refetch();
|
||||||
|
prevSortRef.current = { sortBy, sortDirection };
|
||||||
|
}
|
||||||
|
}, [queryParams, isOpen, refetch]);
|
||||||
|
|
||||||
|
const debouncedApplySearch = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce((value: string) => {
|
||||||
|
setQueryParams((prev) => ({
|
||||||
|
...prev,
|
||||||
|
search: encodeURIComponent(value.trim()),
|
||||||
|
}));
|
||||||
|
}, 500), // Increased debounce time to 500ms for better UX
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setSearchInput(value);
|
||||||
|
debouncedApplySearch(value);
|
||||||
|
},
|
||||||
|
[debouncedApplySearch],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
debouncedApplySearch.cancel();
|
||||||
|
};
|
||||||
|
}, [debouncedApplySearch]);
|
||||||
|
|
||||||
|
const allConversations = useMemo(() => {
|
||||||
|
if (!data?.pages) return [];
|
||||||
|
return data.pages.flatMap((page) => page?.conversations?.filter(Boolean) ?? []);
|
||||||
|
}, [data?.pages]);
|
||||||
|
|
||||||
|
const unarchiveMutation = useArchiveConvoMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
await refetch();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_unarchive_error'),
|
||||||
|
severity: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useDeleteConversationMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_archived_conversation_delete_success'),
|
||||||
|
severity: NotificationSeverity.SUCCESS,
|
||||||
|
});
|
||||||
|
setIsDeleteOpen(false);
|
||||||
|
await refetch();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_archive_delete_error'),
|
||||||
|
severity: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFetchNextPage = useCallback(async () => {
|
||||||
|
if (!hasNextPage || isFetchingNextPage) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchNextPage();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch next page:', error);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_unarchive_error'),
|
||||||
|
severity: NotificationSeverity.ERROR,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [fetchNextPage, hasNextPage, isFetchingNextPage, showToast, localize]);
|
||||||
|
|
||||||
|
const confirmDelete = useCallback(() => {
|
||||||
|
if (!deleteRow?.conversationId) return;
|
||||||
|
deleteMutation.mutate({ conversationId: deleteRow.conversationId });
|
||||||
|
}, [deleteMutation, deleteRow]);
|
||||||
|
|
||||||
|
const { sortBy, sortDirection } = queryParams;
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'title',
|
||||||
|
header: () => (
|
||||||
|
<span className="text-xs sm:text-sm">{localize('com_nav_archive_name')}</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { conversationId, title } = row.original;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 truncate"
|
||||||
|
onClick={() => window.open(`/c/${conversationId}`, '_blank')}
|
||||||
|
>
|
||||||
|
<MinimalIcon
|
||||||
|
endpoint={row.original.endpoint}
|
||||||
|
size={28}
|
||||||
|
isCreatedByUser={false}
|
||||||
|
iconClassName="size-4"
|
||||||
|
/>
|
||||||
|
<span className="underline">{title}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
size: isSmallScreen ? '70%' : '50%',
|
||||||
|
mobileSize: '70%',
|
||||||
|
enableSorting: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'createdAt',
|
||||||
|
header: () => (
|
||||||
|
<span className="text-xs sm:text-sm">{localize('com_nav_archive_created_at')}</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen),
|
||||||
|
meta: {
|
||||||
|
size: isSmallScreen ? '30%' : '35%',
|
||||||
|
mobileSize: '30%',
|
||||||
|
enableSorting: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'actions',
|
||||||
|
header: () => (
|
||||||
|
<Label className="px-2 py-0 text-xs sm:px-2 sm:py-2 sm:text-sm">
|
||||||
|
{localize('com_assistants_actions')}
|
||||||
|
</Label>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const conversation = row.original;
|
||||||
|
const isRowUnarchiving = unarchivingId === conversation.conversationId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TooltipAnchor
|
||||||
|
description={localize('com_ui_unarchive')}
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||||
|
onClick={() => {
|
||||||
|
setUnarchivingId(conversation.conversationId);
|
||||||
|
unarchiveMutation.mutate(
|
||||||
|
{ conversationId: conversation.conversationId, isArchived: false },
|
||||||
|
{ onSettled: () => setUnarchivingId(null) },
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
disabled={isRowUnarchiving}
|
||||||
|
>
|
||||||
|
{isRowUnarchiving ? <Spinner /> : <ArchiveRestore className="size-4" />}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TooltipAnchor
|
||||||
|
description={localize('com_ui_delete')}
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteRow(row.original);
|
||||||
|
setIsDeleteOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
size: '15%',
|
||||||
|
mobileSize: '25%',
|
||||||
|
enableSorting: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[isSmallScreen, localize, unarchivingId, unarchiveMutation],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
@ -16,11 +280,49 @@ export default function ArchivedChats() {
|
||||||
{localize('com_ui_manage')}
|
{localize('com_ui_manage')}
|
||||||
</Button>
|
</Button>
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
|
<OGDialogContent className="w-11/12 max-w-5xl">
|
||||||
|
<OGDialogHeader>
|
||||||
|
<OGDialogTitle>{localize('com_nav_archived_chats')}</OGDialogTitle>
|
||||||
|
</OGDialogHeader>
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={allConversations}
|
||||||
|
filterColumn="title"
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
|
filterValue={searchInput}
|
||||||
|
fetchNextPage={handleFetchNextPage}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
isLoading={isLoading}
|
||||||
|
showCheckboxes={false}
|
||||||
|
enableSearch={!!isSearchEnabled}
|
||||||
|
onSortChange={handleSort}
|
||||||
|
sortBy={sortBy}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
/>
|
||||||
|
</OGDialogContent>
|
||||||
|
</OGDialog>
|
||||||
|
<OGDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
title={localize('com_nav_archived_chats')}
|
showCloseButton={false}
|
||||||
className="max-w-[1000px]"
|
title={localize('com_ui_delete_archived_chats')}
|
||||||
showCancelButton={false}
|
className="w-11/12 max-w-md"
|
||||||
main={<ArchivedChatsTable isOpen={isOpen} onOpenChange={setIsOpen} />}
|
main={
|
||||||
|
<div className="flex w-full flex-col items-center gap-2">
|
||||||
|
<div className="grid w-full items-center gap-2">
|
||||||
|
<Label className="text-left text-sm font-medium">
|
||||||
|
{localize('com_ui_delete_confirm')} <strong>{deleteRow?.title}</strong>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
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 ? <Spinner /> : localize('com_ui_delete'),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</OGDialog>
|
</OGDialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,311 +0,0 @@
|
||||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { TrashIcon, ArchiveRestore, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
OGDialog,
|
|
||||||
OGDialogContent,
|
|
||||||
OGDialogHeader,
|
|
||||||
OGDialogTitle,
|
|
||||||
Label,
|
|
||||||
TooltipAnchor,
|
|
||||||
Spinner,
|
|
||||||
DataTable,
|
|
||||||
useToastContext,
|
|
||||||
useMediaQuery,
|
|
||||||
} from '@librechat/client';
|
|
||||||
import type { ConversationListParams, TConversation } from 'librechat-data-provider';
|
|
||||||
import {
|
|
||||||
useArchiveConvoMutation,
|
|
||||||
useConversationsInfiniteQuery,
|
|
||||||
useDeleteConversationMutation,
|
|
||||||
} from '~/data-provider';
|
|
||||||
import { MinimalIcon } from '~/components/Endpoints';
|
|
||||||
import { NotificationSeverity } from '~/common';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import { formatDate } from '~/utils';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
const DEFAULT_PARAMS: ConversationListParams = {
|
|
||||||
isArchived: true,
|
|
||||||
sortBy: 'createdAt',
|
|
||||||
sortDirection: 'desc',
|
|
||||||
search: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ArchivedChatsTable({
|
|
||||||
onOpenChange,
|
|
||||||
}: {
|
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const localize = useLocalize();
|
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
|
||||||
const { showToast } = useToastContext();
|
|
||||||
const isSearchEnabled = useRecoilValue(store.search);
|
|
||||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
|
||||||
const [queryParams, setQueryParams] = useState<ConversationListParams>(DEFAULT_PARAMS);
|
|
||||||
const [deleteConversation, setDeleteConversation] = useState<TConversation | null>(null);
|
|
||||||
|
|
||||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } =
|
|
||||||
useConversationsInfiniteQuery(queryParams, {
|
|
||||||
staleTime: 0,
|
|
||||||
cacheTime: 5 * 60 * 1000,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => {
|
|
||||||
setQueryParams((prev) => ({
|
|
||||||
...prev,
|
|
||||||
sortBy: sortField as 'title' | 'createdAt',
|
|
||||||
sortDirection: sortOrder,
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleFilterChange = useCallback((value: string) => {
|
|
||||||
const encodedValue = encodeURIComponent(value.trim());
|
|
||||||
setQueryParams((prev) => ({
|
|
||||||
...prev,
|
|
||||||
search: encodedValue,
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const debouncedFilterChange = useMemo(
|
|
||||||
() => debounce(handleFilterChange, 300),
|
|
||||||
[handleFilterChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
debouncedFilterChange.cancel();
|
|
||||||
};
|
|
||||||
}, [debouncedFilterChange]);
|
|
||||||
|
|
||||||
const allConversations = useMemo(() => {
|
|
||||||
if (!data?.pages) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return data.pages.flatMap((page) => page?.conversations?.filter(Boolean) ?? []);
|
|
||||||
}, [data?.pages]);
|
|
||||||
|
|
||||||
const deleteMutation = useDeleteConversationMutation({
|
|
||||||
onSuccess: async () => {
|
|
||||||
setIsDeleteOpen(false);
|
|
||||||
await refetch();
|
|
||||||
},
|
|
||||||
onError: (error: unknown) => {
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_archive_delete_error') as string,
|
|
||||||
severity: NotificationSeverity.ERROR,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const unarchiveMutation = useArchiveConvoMutation({
|
|
||||||
onSuccess: async () => {
|
|
||||||
await refetch();
|
|
||||||
},
|
|
||||||
onError: (error: unknown) => {
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_unarchive_error') as string,
|
|
||||||
severity: NotificationSeverity.ERROR,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFetchNextPage = useCallback(async () => {
|
|
||||||
if (!hasNextPage || isFetchingNextPage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await fetchNextPage();
|
|
||||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
accessorKey: 'title',
|
|
||||||
header: () => {
|
|
||||||
const isSorted = queryParams.sortBy === 'title';
|
|
||||||
const sortDirection = queryParams.sortDirection;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleSort('title', isSorted && sortDirection === 'asc' ? 'desc' : 'asc')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{localize('com_nav_archive_name')}
|
|
||||||
{isSorted && sortDirection === 'asc' && (
|
|
||||||
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
)}
|
|
||||||
{isSorted && sortDirection === 'desc' && (
|
|
||||||
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
)}
|
|
||||||
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const { conversationId, title } = row.original;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-2 truncate"
|
|
||||||
onClick={() => window.open(`/c/${conversationId}`, '_blank')}
|
|
||||||
>
|
|
||||||
<MinimalIcon
|
|
||||||
endpoint={row.original.endpoint}
|
|
||||||
size={28}
|
|
||||||
isCreatedByUser={false}
|
|
||||||
iconClassName="size-4"
|
|
||||||
/>
|
|
||||||
<span className="underline">{title}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
size: isSmallScreen ? '70%' : '50%',
|
|
||||||
mobileSize: '70%',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'createdAt',
|
|
||||||
header: () => {
|
|
||||||
const isSorted = queryParams.sortBy === 'createdAt';
|
|
||||||
const sortDirection = queryParams.sortDirection;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleSort('createdAt', isSorted && sortDirection === 'asc' ? 'desc' : 'asc')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{localize('com_nav_archive_created_at')}
|
|
||||||
{isSorted && sortDirection === 'asc' && (
|
|
||||||
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
)}
|
|
||||||
{isSorted && sortDirection === 'desc' && (
|
|
||||||
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
|
||||||
)}
|
|
||||||
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen),
|
|
||||||
meta: {
|
|
||||||
size: isSmallScreen ? '30%' : '35%',
|
|
||||||
mobileSize: '30%',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'actions',
|
|
||||||
header: () => (
|
|
||||||
<Label className="px-2 py-0 text-xs sm:px-2 sm:py-2 sm:text-sm">
|
|
||||||
{localize('com_assistants_actions')}
|
|
||||||
</Label>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const conversation = row.original;
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<TooltipAnchor
|
|
||||||
description={localize('com_ui_unarchive')}
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
|
||||||
onClick={() =>
|
|
||||||
unarchiveMutation.mutate({
|
|
||||||
conversationId: conversation.conversationId,
|
|
||||||
isArchived: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
title={localize('com_ui_unarchive')}
|
|
||||||
disabled={unarchiveMutation.isLoading}
|
|
||||||
>
|
|
||||||
{unarchiveMutation.isLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<ArchiveRestore className="size-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TooltipAnchor
|
|
||||||
description={localize('com_ui_delete')}
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
|
||||||
onClick={() => {
|
|
||||||
setDeleteConversation(row.original);
|
|
||||||
setIsDeleteOpen(true);
|
|
||||||
}}
|
|
||||||
title={localize('com_ui_delete')}
|
|
||||||
>
|
|
||||||
<TrashIcon className="size-4" />
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
size: '15%',
|
|
||||||
mobileSize: '25%',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[handleSort, isSmallScreen, localize, queryParams, unarchiveMutation],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DataTable
|
|
||||||
columns={columns}
|
|
||||||
data={allConversations}
|
|
||||||
filterColumn="title"
|
|
||||||
onFilterChange={debouncedFilterChange}
|
|
||||||
filterValue={queryParams.search}
|
|
||||||
fetchNextPage={handleFetchNextPage}
|
|
||||||
hasNextPage={hasNextPage}
|
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
|
||||||
isLoading={isLoading}
|
|
||||||
showCheckboxes={false}
|
|
||||||
enableSearch={isSearchEnabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<OGDialog open={isDeleteOpen} onOpenChange={onOpenChange}>
|
|
||||||
<OGDialogContent
|
|
||||||
title={localize('com_ui_delete_confirm') + ' ' + (deleteConversation?.title ?? '')}
|
|
||||||
className="w-11/12 max-w-md"
|
|
||||||
>
|
|
||||||
<OGDialogHeader>
|
|
||||||
<OGDialogTitle>
|
|
||||||
{localize('com_ui_delete_confirm')} <strong>{deleteConversation?.title}</strong>
|
|
||||||
</OGDialogTitle>
|
|
||||||
</OGDialogHeader>
|
|
||||||
<div className="flex justify-end gap-4 pt-4">
|
|
||||||
<Button aria-label="cancel" variant="outline" onClick={() => setIsDeleteOpen(false)}>
|
|
||||||
{localize('com_ui_cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() =>
|
|
||||||
deleteMutation.mutate({
|
|
||||||
conversationId: deleteConversation?.conversationId ?? '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disabled={deleteMutation.isLoading}
|
|
||||||
>
|
|
||||||
{deleteMutation.isLoading ? <Spinner /> : localize('com_ui_delete')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</OGDialogContent>
|
|
||||||
</OGDialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"chat_direction_left_to_right": "something needs to go here. was empty",
|
"chat_direction_left_to_right": "Chat direction set to left to right",
|
||||||
"chat_direction_right_to_left": "something needs to go here. was empty",
|
"chat_direction_right_to_left": "Chat direction set to right to left",
|
||||||
"com_a11y_ai_composing": "The AI is still composing.",
|
"com_a11y_ai_composing": "The AI is still composing.",
|
||||||
"com_a11y_end": "The AI has finished their reply.",
|
"com_a11y_end": "The AI has finished their reply.",
|
||||||
"com_a11y_start": "The AI has started their reply.",
|
"com_a11y_start": "The AI has started their reply.",
|
||||||
|
@ -389,7 +389,7 @@
|
||||||
"com_files_number_selected": "{{0}} of {{1}} items selected",
|
"com_files_number_selected": "{{0}} of {{1}} items selected",
|
||||||
"com_files_preparing_download": "Preparing download...",
|
"com_files_preparing_download": "Preparing download...",
|
||||||
"com_files_sharepoint_picker_title": "Pick Files",
|
"com_files_sharepoint_picker_title": "Pick Files",
|
||||||
"com_files_table": "something needs to go here. was empty",
|
"com_files_table": "Files Table",
|
||||||
"com_files_upload_local_machine": "From Local Computer",
|
"com_files_upload_local_machine": "From Local Computer",
|
||||||
"com_files_upload_sharepoint": "From SharePoint",
|
"com_files_upload_sharepoint": "From SharePoint",
|
||||||
"com_generated_files": "Generated files:",
|
"com_generated_files": "Generated files:",
|
||||||
|
@ -747,7 +747,8 @@
|
||||||
"com_ui_bookmarks_title": "Title",
|
"com_ui_bookmarks_title": "Title",
|
||||||
"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_update_success": "Bookmark updated successfully",
|
"com_ui_bookmarks_update_success": "Bookmark updated successfully",
|
||||||
"com_ui_bulk_delete_error": "Failed to delete shared links",
|
"com_ui_shared_link_delete_error": "Failed to delete shared link",
|
||||||
|
"com_ui_archived_chat_delete_error": "Failed to delete archived chat",
|
||||||
"com_ui_callback_url": "Callback URL",
|
"com_ui_callback_url": "Callback URL",
|
||||||
"com_ui_cancel": "Cancel",
|
"com_ui_cancel": "Cancel",
|
||||||
"com_ui_cancelled": "Cancelled",
|
"com_ui_cancelled": "Cancelled",
|
||||||
|
@ -830,6 +831,7 @@
|
||||||
"com_ui_delete_not_allowed": "Delete operation is not allowed",
|
"com_ui_delete_not_allowed": "Delete operation is not allowed",
|
||||||
"com_ui_delete_prompt": "Delete Prompt?",
|
"com_ui_delete_prompt": "Delete Prompt?",
|
||||||
"com_ui_delete_shared_link": "Delete shared link?",
|
"com_ui_delete_shared_link": "Delete shared link?",
|
||||||
|
"com_ui_delete_archived_chats": "Delete archived chat?",
|
||||||
"com_ui_delete_success": "Successfully deleted",
|
"com_ui_delete_success": "Successfully deleted",
|
||||||
"com_ui_delete_tool": "Delete Tool",
|
"com_ui_delete_tool": "Delete Tool",
|
||||||
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
|
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
|
||||||
|
@ -848,7 +850,7 @@
|
||||||
"com_ui_download_backup": "Download Backup Codes",
|
"com_ui_download_backup": "Download Backup Codes",
|
||||||
"com_ui_download_backup_tooltip": "Before you continue, download your backup codes. You will need them to regain access if you lose your authenticator device",
|
"com_ui_download_backup_tooltip": "Before you continue, download your backup codes. You will need them to regain access if you lose your authenticator device",
|
||||||
"com_ui_download_error": "Error downloading file. The file may have been deleted.",
|
"com_ui_download_error": "Error downloading file. The file may have been deleted.",
|
||||||
"com_ui_drag_drop": "something needs to go here. was empty",
|
"com_ui_drag_drop_image": "Drag and drop an image here",
|
||||||
"com_ui_dropdown_variables": "Dropdown variables:",
|
"com_ui_dropdown_variables": "Dropdown variables:",
|
||||||
"com_ui_dropdown_variables_info": "Create custom dropdown menus for your prompts: `{{variable_name:option1|option2|option3}}`",
|
"com_ui_dropdown_variables_info": "Create custom dropdown menus for your prompts: `{{variable_name:option1|option2|option3}}`",
|
||||||
"com_ui_duplicate": "Duplicate",
|
"com_ui_duplicate": "Duplicate",
|
||||||
|
@ -1025,7 +1027,7 @@
|
||||||
"com_ui_no_categories": "No categories available",
|
"com_ui_no_categories": "No categories available",
|
||||||
"com_ui_no_category": "No category",
|
"com_ui_no_category": "No category",
|
||||||
"com_ui_no_changes": "No changes were made",
|
"com_ui_no_changes": "No changes were made",
|
||||||
"com_ui_no_data": "something needs to go here. was empty",
|
"com_ui_no_data": "No data",
|
||||||
"com_ui_no_individual_access": "No individual users or groups have access to this agent",
|
"com_ui_no_individual_access": "No individual users or groups have access to this agent",
|
||||||
"com_ui_no_personalization_available": "No personalization options are currently available",
|
"com_ui_no_personalization_available": "No personalization options are currently available",
|
||||||
"com_ui_no_read_access": "You don't have permission to view memories",
|
"com_ui_no_read_access": "You don't have permission to view memories",
|
||||||
|
@ -1160,6 +1162,7 @@
|
||||||
"com_ui_share_var": "Share {{0}}",
|
"com_ui_share_var": "Share {{0}}",
|
||||||
"com_ui_shared_link_bulk_delete_success": "Successfully deleted shared links",
|
"com_ui_shared_link_bulk_delete_success": "Successfully deleted shared links",
|
||||||
"com_ui_shared_link_delete_success": "Successfully deleted shared link",
|
"com_ui_shared_link_delete_success": "Successfully deleted shared link",
|
||||||
|
"com_ui_archived_conversation_delete_success": "Successfully deleted archived conversation",
|
||||||
"com_ui_shared_link_not_found": "Shared link not found",
|
"com_ui_shared_link_not_found": "Shared link not found",
|
||||||
"com_ui_shared_prompts": "Shared Prompts",
|
"com_ui_shared_prompts": "Shared Prompts",
|
||||||
"com_ui_shop": "Shopping",
|
"com_ui_shop": "Shopping",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState, memo, useMemo } from 'react';
|
import React, { useCallback, useEffect, useRef, useState, memo, useMemo } from 'react';
|
||||||
|
import { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
|
@ -387,25 +388,38 @@ export default function DataTable<TData, TValue>({
|
||||||
<TableHeader className="sticky top-0 z-50 bg-surface-secondary">
|
<TableHeader className="sticky top-0 z-50 bg-surface-secondary">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id} className="border-b border-border-light">
|
<TableRow key={headerGroup.id} className="border-b border-border-light">
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => {
|
||||||
<TableHead
|
const sortDir = header.column.getIsSorted();
|
||||||
key={header.id}
|
const canSort = header.column.getCanSort();
|
||||||
className="whitespace-nowrap bg-surface-secondary px-2 py-2 text-left text-sm font-medium text-text-secondary sm:px-4"
|
return (
|
||||||
style={getColumnStyle(
|
<TableHead
|
||||||
header.column.columnDef as TableColumn<TData, TValue>,
|
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
|
||||||
isSmallScreen,
|
key={header.id}
|
||||||
)}
|
className="relative cursor-pointer whitespace-nowrap bg-surface-secondary px-2 py-2 text-left text-sm font-medium text-text-secondary hover:bg-surface-hover sm:px-4"
|
||||||
onClick={
|
style={getColumnStyle(
|
||||||
header.column.getCanSort()
|
header.column.columnDef as TableColumn<TData, TValue>,
|
||||||
? header.column.getToggleSortingHandler()
|
isSmallScreen,
|
||||||
: undefined
|
)}
|
||||||
}
|
>
|
||||||
>
|
<div className="flex items-center">
|
||||||
{header.isPlaceholder
|
<span className="flex-1 text-left">
|
||||||
? null
|
{header.isPlaceholder
|
||||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
? null
|
||||||
</TableHead>
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
))}
|
</span>
|
||||||
|
{canSort && (
|
||||||
|
<span className="ml-1">
|
||||||
|
{sortDir === false && (
|
||||||
|
<ChevronsUpDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{sortDir === 'asc' && <ChevronUp className="h-4 w-4 text-primary" />}
|
||||||
|
{sortDir === 'desc' && <ChevronDown className="h-4 w-4 text-primary" />}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"com_ui_cancel": "Cancel",
|
"com_ui_cancel": "Cancel",
|
||||||
"com_ui_no_options": "No options available"
|
"com_ui_no_options": "No options available",
|
||||||
|
"com_ui_no_data": "No Data Available"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue