Quellcode durchsuchen

Feat: Add FilesTable #3221 (#4491)

### What problem does this PR solve?

Feat: Add FilesTable #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.16.0
balibabu vor 9 Monaten
Ursprung
Commit
b4614e9517
Es ist kein Account mit der E-Mail-Adresse des Committers verbunden

+ 8
- 5
web/src/app.tsx Datei anzeigen

import weekday from 'dayjs/plugin/weekday'; import weekday from 'dayjs/plugin/weekday';
import React, { ReactNode, useEffect, useState } from 'react'; import React, { ReactNode, useEffect, useState } from 'react';
import { ThemeProvider, useTheme } from './components/theme-provider'; import { ThemeProvider, useTheme } from './components/theme-provider';
import { TooltipProvider } from './components/ui/tooltip';
import storage from './utils/authorization-util'; import storage from './utils/authorization-util';


dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
}, []); }, []);


return ( return (
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="light" storageKey="ragflow-ui-theme">
<Root>{children}</Root>
</ThemeProvider>
</QueryClientProvider>
<TooltipProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="light" storageKey="ragflow-ui-theme">
<Root>{children}</Root>
</ThemeProvider>
</QueryClientProvider>
</TooltipProvider>
); );
}; };
export function rootContainer(container: ReactNode) { export function rootContainer(container: ReactNode) {

+ 3
- 2
web/src/components/list-filter-bar.tsx Datei anzeigen

import { Filter, Search } from 'lucide-react';
import { Filter } from 'lucide-react';
import { PropsWithChildren } from 'react'; import { PropsWithChildren } from 'react';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { SearchInput } from './ui/input';


interface IProps { interface IProps {
title: string; title: string;
<span className="text-3xl font-bold ">{title}</span> <span className="text-3xl font-bold ">{title}</span>
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<Filter className="size-5" /> <Filter className="size-5" />
<Search className="size-5" />
<SearchInput></SearchInput>
<Button variant={'tertiary'} size={'sm'} onClick={showDialog}> <Button variant={'tertiary'} size={'sm'} onClick={showDialog}>
{children} {children}
</Button> </Button>

+ 13
- 0
web/src/components/skeleton-card.tsx Datei anzeigen

import { Skeleton } from '@/components/ui/skeleton';

export function SkeletonCard() {
return (
<div className="flex flex-col space-y-3 items-center">
<Skeleton className="h-[125px] w-[250px] rounded-xl" />
<div className="space-y-2 w-[250px]">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
);
}

+ 27
- 0
web/src/components/table-skeleton.tsx Datei anzeigen

import { PropsWithChildren } from 'react';
import { SkeletonCard } from './skeleton-card';
import { TableCell, TableRow } from './ui/table';

type IProps = { columnsLength: number };

function Row({ children, columnsLength }: PropsWithChildren & IProps) {
return (
<TableRow>
<TableCell colSpan={columnsLength} className="h-24 text-center ">
{children}
</TableCell>
</TableRow>
);
}

export function TableSkeleton({ columnsLength }: { columnsLength: number }) {
return (
<Row columnsLength={columnsLength}>
<SkeletonCard></SkeletonCard>
</Row>
);
}

export function TableEmpty({ columnsLength }: { columnsLength: number }) {
return <Row columnsLength={columnsLength}>No results.</Row>;
}

+ 36
- 1
web/src/components/ui/input.tsx Datei anzeigen

import * as React from 'react'; import * as React from 'react';


import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Search } from 'lucide-react';


export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {} extends React.InputHTMLAttributes<HTMLInputElement> {}
); );
Input.displayName = 'Input'; Input.displayName = 'Input';


export { Input };
export interface ExpandedInputProps extends Omit<InputProps, 'prefix'> {
prefix?: React.ReactNode;
suffix?: React.ReactNode;
}

const ExpandedInput = ({ suffix, prefix, ...props }: ExpandedInputProps) => {
return (
<div className="relative">
<span
className={cn({
['absolute left-3 top-[50%] translate-y-[-50%]']: prefix,
})}
>
{prefix}
</span>
<Input
className={cn({ 'pr-10': suffix, 'pl-10': prefix })}
{...props}
></Input>
<span
className={cn({
['absolute right-3 top-[50%] translate-y-[-50%]']: suffix,
})}
>
{suffix}
</span>
</div>
);
};

const SearchInput = (props: InputProps) => {
return <ExpandedInput suffix={<Search />} {...props}></ExpandedInput>;
};

export { ExpandedInput, Input, SearchInput };

+ 2
- 1
web/src/layouts/next-header.tsx Datei anzeigen

import { import {
ChevronDown, ChevronDown,
Cpu, Cpu,
File,
Github, Github,
House, House,
Library, Library,
{ path: Routes.Chat, name: t('chat'), icon: MessageSquareText }, { path: Routes.Chat, name: t('chat'), icon: MessageSquareText },
{ path: Routes.Search, name: t('search'), icon: Search }, { path: Routes.Search, name: t('search'), icon: Search },
{ path: Routes.Agent, name: t('flow'), icon: Cpu }, { path: Routes.Agent, name: t('flow'), icon: Cpu },
// { path: '/file', name: t('fileManager'), icon: FileIcon },
{ path: Routes.Files, name: t('fileManager'), icon: File },
], ],
[t], [t],
); );

+ 1
- 1
web/src/pages/dataset/dataset/index.tsx Datei anzeigen

documentUploadLoading, documentUploadLoading,
} = useHandleUploadDocument(); } = useHandleUploadDocument();
return ( return (
<section className="p-8 text-foreground">
<section className="p-8">
<ListFilterBar title="Files" showDialog={showDocumentUploadModal}> <ListFilterBar title="Files" showDialog={showDocumentUploadModal}>
<Upload /> <Upload />
Upload file Upload file

+ 1
- 1
web/src/pages/datasets/dataset-creating-dialog.tsx Datei anzeigen

<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="w-2/3 space-y-6"
className="space-y-6"
id={FormId} id={FormId}
> >
<FormField <FormField

+ 10
- 5
web/src/pages/file-manager/action-cell/index.tsx Datei anzeigen

import NewDocumentLink from '@/components/new-document-link'; import NewDocumentLink from '@/components/new-document-link';
import SvgIcon from '@/components/svg-icon';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useDownloadFile } from '@/hooks/file-manager-hooks'; import { useDownloadFile } from '@/hooks/file-manager-hooks';
import { IFile } from '@/interfaces/database/file-manager'; import { IFile } from '@/interfaces/database/file-manager';
isSupportedPreviewDocumentType, isSupportedPreviewDocumentType,
} from '@/utils/document-util'; } from '@/utils/document-util';
import { import {
DeleteOutlined,
DownloadOutlined, DownloadOutlined,
EditOutlined, EditOutlined,
EyeOutlined, EyeOutlined,
LinkOutlined, LinkOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Button, Space, Tooltip } from 'antd'; import { Button, Space, Tooltip } from 'antd';
import { FolderInput, Trash2 } from 'lucide-react';
import { useHandleDeleteFile } from '../hooks'; import { useHandleDeleteFile } from '../hooks';


interface IProps { interface IProps {
type="text" type="text"
disabled={beingUsed} disabled={beingUsed}
onClick={onShowMoveFileModal} onClick={onShowMoveFileModal}
className="flex items-end"
> >
<SvgIcon name={`move`} width={16}></SvgIcon>
<FolderInput className="size-4" />
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
{isKnowledgeBase || ( {isKnowledgeBase || (
<Tooltip title={t('delete', { keyPrefix: 'common' })}> <Tooltip title={t('delete', { keyPrefix: 'common' })}>
<Button type="text" disabled={beingUsed} onClick={handleRemoveFile}>
<DeleteOutlined size={20} />
<Button
type="text"
disabled={beingUsed}
onClick={handleRemoveFile}
className="flex items-end"
>
<Trash2 className="size-4" />
</Button> </Button>
</Tooltip> </Tooltip>
)} )}

+ 5
- 6
web/src/pages/file-manager/file-toolbar.tsx Datei anzeigen

import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg';
import SvgIcon from '@/components/svg-icon';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { import {
IListResult, IListResult,
useSelectBreadcrumbItems, useSelectBreadcrumbItems,
} from './hooks'; } from './hooks';


import { FolderInput, Trash2 } from 'lucide-react';
import styles from './index.less'; import styles from './index.less';


interface IProps interface IProps
onClick: handleRemoveFile, onClick: handleRemoveFile,
label: ( label: (
<Flex gap={10}> <Flex gap={10}>
<span className={styles.deleteIconWrapper}>
<DeleteIcon width={18} />
<span className="flex items-center justify-center">
<Trash2 className="size-4" />
</span> </span>
<b>{t('delete', { keyPrefix: 'common' })}</b> <b>{t('delete', { keyPrefix: 'common' })}</b>
</Flex> </Flex>
onClick: handleShowMoveFileModal, onClick: handleShowMoveFileModal,
label: ( label: (
<Flex gap={10}> <Flex gap={10}>
<span className={styles.deleteIconWrapper}>
<SvgIcon name={`move`} width={18}></SvgIcon>
<span className="flex items-center justify-center">
<FolderInput className="size-4"></FolderInput>
</span> </span>
<b>{t('move', { keyPrefix: 'common' })}</b> <b>{t('move', { keyPrefix: 'common' })}</b>
</Flex> </Flex>

+ 343
- 0
web/src/pages/files/files-table.tsx Datei anzeigen

'use client';

import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { ArrowUpDown, MoreHorizontal, Pencil } from 'lucide-react';
import * as React from 'react';

import SvgIcon from '@/components/svg-icon';
import { TableEmpty, TableSkeleton } from '@/components/table-skeleton';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Switch } from '@/components/ui/switch';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useFetchFileList } from '@/hooks/file-manager-hooks';
import { IFile } from '@/interfaces/database/file-manager';
import { cn } from '@/lib/utils';
import { formatFileSize } from '@/utils/common-util';
import { formatDate } from '@/utils/date';
import { getExtension } from '@/utils/document-util';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigateToOtherFolder } from './hooks';

export function FilesTable() {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const { t } = useTranslation('translation', {
keyPrefix: 'fileManager',
});
const navigateToOtherFolder = useNavigateToOtherFolder();

const { pagination, data, loading, setPagination } = useFetchFileList();

const columns: ColumnDef<IFile>[] = [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'name',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
{t('name')}
<ArrowUpDown />
</Button>
);
},
meta: { cellClassName: 'max-w-[20vw]' },
cell: ({ row }) => {
const name: string = row.getValue('name');
const type = row.original.type;
const id = row.original.id;
const isFolder = type === 'folder';

const handleNameClick = () => {
if (isFolder) {
navigateToOtherFolder(id);
}
};

return (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex gap-2">
<SvgIcon
name={`file-icon/${isFolder ? 'folder' : getExtension(name)}`}
width={24}
></SvgIcon>
<span
className={cn('truncate', { ['cursor-pointer']: isFolder })}
onClick={handleNameClick}
>
{name}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{name}</p>
</TooltipContent>
</Tooltip>
);
},
},
{
accessorKey: 'create_time',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
{t('uploadDate')}
<ArrowUpDown />
</Button>
);
},
cell: ({ row }) => (
<div className="lowercase">
{formatDate(row.getValue('create_time'))}
</div>
),
},
{
accessorKey: 'size',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
{t('size')}
<ArrowUpDown />
</Button>
);
},
cell: ({ row }) => (
<div className="capitalize">{formatFileSize(row.getValue('size'))}</div>
),
},
{
accessorKey: 'kbs_info',
header: t('knowledgeBase'),
cell: ({ row }) => (
<Button variant="destructive" size={'sm'}>
{row.getValue('kbs_info')}
</Button>
),
},
{
id: 'actions',
header: t('action'),
enableHiding: false,
cell: ({ row }) => {
const payment = row.original;

return (
<section className="flex gap-4 items-center">
<Switch id="airplane-mode" />
<Button variant="secondary" size={'icon'}>
<Pencil />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size={'icon'}>
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(payment.id)}
>
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</section>
);
},
},
];

const currentPagination = useMemo(() => {
return {
pageIndex: (pagination.current || 1) - 1,
pageSize: pagination.pageSize || 10,
};
}, [pagination]);

const table = useReactTable({
data: data?.files || [],
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
// getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: (updaterOrValue: any) => {
if (typeof updaterOrValue === 'function') {
const nextPagination = updaterOrValue(currentPagination);
setPagination({
page: nextPagination.pageIndex + 1,
pageSize: nextPagination.pageSize,
});
} else {
setPagination({
page: updaterOrValue.pageIndex,
pageSize: updaterOrValue.pageSize,
});
}
},
manualPagination: true, //we're doing manual "server-side" pagination

state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination: currentPagination,
},
rowCount: data?.total ?? 0,
debugTable: true,
});

return (
<div className="w-full">
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{loading ? (
<TableSkeleton columnsLength={columns.length}></TableSkeleton>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cell.column.columnDef.meta?.cellClassName}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableEmpty columnsLength={columns.length}></TableEmpty>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of {data?.total}{' '}
row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}

+ 294
- 0
web/src/pages/files/hooks.ts Datei anzeigen

import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import {
useConnectToKnowledge,
useCreateFolder,
useDeleteFile,
useFetchParentFolderList,
useMoveFile,
useRenameFile,
useUploadFile,
} from '@/hooks/file-manager-hooks';
import { IFile } from '@/interfaces/database/file-manager';
import { TableRowSelection } from 'antd/es/table/interface';
import { UploadFile } from 'antd/lib';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'umi';

export const useGetFolderId = () => {
const [searchParams] = useSearchParams();
const id = searchParams.get('folderId') as string;

return id ?? '';
};

export const useGetRowSelection = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);

const rowSelection: TableRowSelection<IFile> = {
selectedRowKeys,
getCheckboxProps: (record) => {
return { disabled: record.source_type === 'knowledgebase' };
},
onChange: (newSelectedRowKeys: React.Key[]) => {
setSelectedRowKeys(newSelectedRowKeys);
},
};

return { rowSelection, setSelectedRowKeys };
};

export const useNavigateToOtherFolder = () => {
const navigate = useNavigate();
const navigateToOtherFolder = useCallback(
(folderId: string) => {
navigate(`/file?folderId=${folderId}`);
},
[navigate],
);

return navigateToOtherFolder;
};

export const useRenameCurrentFile = () => {
const [file, setFile] = useState<IFile>({} as IFile);
const {
visible: fileRenameVisible,
hideModal: hideFileRenameModal,
showModal: showFileRenameModal,
} = useSetModalState();
const { renameFile, loading } = useRenameFile();

const onFileRenameOk = useCallback(
async (name: string) => {
const ret = await renameFile({
fileId: file.id,
name,
});

if (ret === 0) {
hideFileRenameModal();
}
},
[renameFile, file, hideFileRenameModal],
);

const handleShowFileRenameModal = useCallback(
async (record: IFile) => {
setFile(record);
showFileRenameModal();
},
[showFileRenameModal],
);

return {
fileRenameLoading: loading,
initialFileName: file.name,
onFileRenameOk,
fileRenameVisible,
hideFileRenameModal,
showFileRenameModal: handleShowFileRenameModal,
};
};

export const useSelectBreadcrumbItems = () => {
const parentFolderList = useFetchParentFolderList();

return parentFolderList.length === 1
? []
: parentFolderList.map((x) => ({
title: x.name === '/' ? 'root' : x.name,
path: `/file?folderId=${x.id}`,
}));
};

export const useHandleCreateFolder = () => {
const {
visible: folderCreateModalVisible,
hideModal: hideFolderCreateModal,
showModal: showFolderCreateModal,
} = useSetModalState();
const { createFolder, loading } = useCreateFolder();
const id = useGetFolderId();

const onFolderCreateOk = useCallback(
async (name: string) => {
const ret = await createFolder({ parentId: id, name });

if (ret === 0) {
hideFolderCreateModal();
}
},
[createFolder, hideFolderCreateModal, id],
);

return {
folderCreateLoading: loading,
onFolderCreateOk,
folderCreateModalVisible,
hideFolderCreateModal,
showFolderCreateModal,
};
};

export const useHandleDeleteFile = (
fileIds: string[],
setSelectedRowKeys: (keys: string[]) => void,
) => {
const { deleteFile: removeDocument } = useDeleteFile();
const showDeleteConfirm = useShowDeleteConfirm();
const parentId = useGetFolderId();

const handleRemoveFile = () => {
showDeleteConfirm({
onOk: async () => {
const code = await removeDocument({ fileIds, parentId });
if (code === 0) {
setSelectedRowKeys([]);
}
return;
},
});
};

return { handleRemoveFile };
};

export const useHandleUploadFile = () => {
const {
visible: fileUploadVisible,
hideModal: hideFileUploadModal,
showModal: showFileUploadModal,
} = useSetModalState();
const { uploadFile, loading } = useUploadFile();
const id = useGetFolderId();

const onFileUploadOk = useCallback(
async (fileList: UploadFile[]): Promise<number | undefined> => {
if (fileList.length > 0) {
const ret: number = await uploadFile({ fileList, parentId: id });
if (ret === 0) {
hideFileUploadModal();
}
return ret;
}
},
[uploadFile, hideFileUploadModal, id],
);

return {
fileUploadLoading: loading,
onFileUploadOk,
fileUploadVisible,
hideFileUploadModal,
showFileUploadModal,
};
};

export const useHandleConnectToKnowledge = () => {
const {
visible: connectToKnowledgeVisible,
hideModal: hideConnectToKnowledgeModal,
showModal: showConnectToKnowledgeModal,
} = useSetModalState();
const { connectFileToKnowledge: connectToKnowledge, loading } =
useConnectToKnowledge();
const [record, setRecord] = useState<IFile>({} as IFile);

const initialValue = useMemo(() => {
return Array.isArray(record?.kbs_info)
? record?.kbs_info?.map((x) => x.kb_id)
: [];
}, [record?.kbs_info]);

const onConnectToKnowledgeOk = useCallback(
async (knowledgeIds: string[]) => {
const ret = await connectToKnowledge({
fileIds: [record.id],
kbIds: knowledgeIds,
});

if (ret === 0) {
hideConnectToKnowledgeModal();
}
return ret;
},
[connectToKnowledge, hideConnectToKnowledgeModal, record.id],
);

const handleShowConnectToKnowledgeModal = useCallback(
(record: IFile) => {
setRecord(record);
showConnectToKnowledgeModal();
},
[showConnectToKnowledgeModal],
);

return {
initialValue,
connectToKnowledgeLoading: loading,
onConnectToKnowledgeOk,
connectToKnowledgeVisible,
hideConnectToKnowledgeModal,
showConnectToKnowledgeModal: handleShowConnectToKnowledgeModal,
};
};

export const useHandleBreadcrumbClick = () => {
const navigate = useNavigate();

const handleBreadcrumbClick = useCallback(
(path?: string) => {
if (path) {
navigate(path);
}
},
[navigate],
);

return { handleBreadcrumbClick };
};

export const useHandleMoveFile = (
setSelectedRowKeys: (keys: string[]) => void,
) => {
const {
visible: moveFileVisible,
hideModal: hideMoveFileModal,
showModal: showMoveFileModal,
} = useSetModalState();
const { moveFile, loading } = useMoveFile();
const [sourceFileIds, setSourceFileIds] = useState<string[]>([]);

const onMoveFileOk = useCallback(
async (targetFolderId: string) => {
const ret = await moveFile({
src_file_ids: sourceFileIds,
dest_file_id: targetFolderId,
});

if (ret === 0) {
setSelectedRowKeys([]);
hideMoveFileModal();
}
return ret;
},
[moveFile, hideMoveFileModal, sourceFileIds, setSelectedRowKeys],
);

const handleShowMoveFileModal = useCallback(
(ids: string[]) => {
setSourceFileIds(ids);
showMoveFileModal();
},
[showMoveFileModal],
);

return {
initialValue: '',
moveFileLoading: loading,
onMoveFileOk,
moveFileVisible,
hideMoveFileModal,
showMoveFileModal: handleShowMoveFileModal,
};
};

+ 15
- 0
web/src/pages/files/index.tsx Datei anzeigen

import ListFilterBar from '@/components/list-filter-bar';
import { Upload } from 'lucide-react';
import { FilesTable } from './files-table';

export default function Files() {
return (
<section className="p-8">
<ListFilterBar title="Files">
<Upload />
Upload file
</ListFilterBar>
<FilesTable></FilesTable>
</section>
);
}

+ 12
- 17
web/src/pages/flow/canvas/index.tsx Datei anzeigen

import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { import {
<Background /> <Background />
<Controls> <Controls>
<ControlButton onClick={handleImportJson}> <ControlButton onClick={handleImportJson}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<FolderInput className={controlIconClassname} />
</TooltipTrigger>
<TooltipContent>Import</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<FolderInput className={controlIconClassname} />
</TooltipTrigger>
<TooltipContent>Import</TooltipContent>
</Tooltip>
</ControlButton> </ControlButton>
<ControlButton onClick={handleExportJson}> <ControlButton onClick={handleExportJson}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<FolderOutput className={controlIconClassname} />
</TooltipTrigger>
<TooltipContent>Export</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<FolderOutput className={controlIconClassname} />
</TooltipTrigger>
<TooltipContent>Export</TooltipContent>
</Tooltip>
</ControlButton> </ControlButton>
</Controls> </Controls>
</ReactFlow> </ReactFlow>

+ 6
- 9
web/src/pages/flow/flow-tooltip.tsx Datei anzeigen

import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { PropsWithChildren } from 'react'; import { PropsWithChildren } from 'react';
export const RunTooltip = ({ children }: PropsWithChildren) => { export const RunTooltip = ({ children }: PropsWithChildren) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipContent>
<p>{t('flow.testRun')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipContent>
<p>{t('flow.testRun')}</p>
</TooltipContent>
</Tooltip>
); );
}; };

+ 1
- 1
web/src/pages/knowledge/knowledge-card/index.tsx Datei anzeigen



return ( return (
<Badge.Ribbon <Badge.Ribbon
text={item.nickname}
text={item?.nickname}
color={userInfo.nickname === item.nickname ? '#1677ff' : 'pink'} color={userInfo.nickname === item.nickname ? '#1677ff' : 'pink'}
className={classNames(styles.ribbon, { className={classNames(styles.ribbon, {
[styles.hideRibbon]: item.permission !== 'team', [styles.hideRibbon]: item.permission !== 'team',

+ 2
- 2
web/src/pages/login-next/index.tsx Datei anzeigen

const step = Number((searchParams.get('step') ?? Step.SignIn) as Step); const step = Number((searchParams.get('step') ?? Step.SignIn) as Step);


return ( return (
<div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')]">
<div className="inline-block">
<div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')] bg-cover bg-center">
<div className="inline-block bg-colors-background-neutral-standard rounded-lg">
{step === Step.SignIn && <SignInCard></SignInCard>} {step === Step.SignIn && <SignInCard></SignInCard>}
{step === Step.SignUp && <SignUpCard></SignUpCard>} {step === Step.SignUp && <SignUpCard></SignUpCard>}
{step === Step.VerifyEmail && <VerifyEmailCard></VerifyEmailCard>} {step === Step.VerifyEmail && <VerifyEmailCard></VerifyEmailCard>}

+ 12
- 0
web/src/routes.ts Datei anzeigen

Agent = '/agent', Agent = '/agent',
Search = '/next-search', Search = '/next-search',
Chat = '/next-chat', Chat = '/next-chat',
Files = '/files',
ProfileSetting = '/profile-setting', ProfileSetting = '/profile-setting',
DatasetTesting = '/testing', DatasetTesting = '/testing',
DatasetSetting = '/setting', DatasetSetting = '/setting',
}, },
], ],
}, },
{
path: Routes.Files,
layout: false,
component: '@/layouts/next',
routes: [
{
path: Routes.Files,
component: `@/pages${Routes.Files}`,
},
],
},
{ {
path: Routes.DatasetBase, path: Routes.DatasetBase,
layout: false, layout: false,

+ 25
- 0
web/src/utils/common-util.ts Datei anzeigen



return view.buffer; return view.buffer;
} }

export function formatFileSize(bytes: number, si = true, dp = 1) {
let nextBytes = bytes;
const thresh = si ? 1000 : 1024;

if (Math.abs(bytes) < thresh) {
return nextBytes + ' B';
}

const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10 ** dp;

do {
nextBytes /= thresh;
++u;
} while (
Math.round(Math.abs(nextBytes) * r) / r >= thresh &&
u < units.length - 1
);

return nextBytes.toFixed(dp) + ' ' + units[u];
}

+ 8
- 0
web/typings.d.ts Datei anzeigen

import '@tanstack/react-table';
declare module 'lodash'; declare module 'lodash';


declare global { declare global {
type Nullable<T> = T | null; type Nullable<T> = T | null;
} }

declare module '@tanstack/react-table' {
interface ColumnMeta {
headerClassName?: string;
cellClassName?: string;
}
}

Laden…
Abbrechen
Speichern