### What problem does this PR solve? Feat: Bind data to the agent module of the home page #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.0
| @@ -1,3 +1,4 @@ | |||
| import { cn } from '@/lib/utils'; | |||
| import { ChevronDown } from 'lucide-react'; | |||
| import React, { | |||
| ChangeEventHandler, | |||
| @@ -38,7 +39,10 @@ export default function ListFilterBar({ | |||
| value, | |||
| onChange, | |||
| filters, | |||
| }: PropsWithChildren<IProps & Omit<CheckboxFormMultipleProps, 'setOpen'>>) { | |||
| className, | |||
| }: PropsWithChildren<IProps & Omit<CheckboxFormMultipleProps, 'setOpen'>> & { | |||
| className?: string; | |||
| }) { | |||
| const filterCount = useMemo(() => { | |||
| return typeof value === 'object' && value !== null | |||
| ? Object.values(value).reduce((pre, cur) => { | |||
| @@ -48,7 +52,7 @@ export default function ListFilterBar({ | |||
| }, [value]); | |||
| return ( | |||
| <div className="flex justify-between mb-6 items-center"> | |||
| <div className={cn('flex justify-between mb-6 items-center', className)}> | |||
| <span className="text-3xl font-bold ">{leftPanel || title}</span> | |||
| <div className="flex gap-4 items-center"> | |||
| {showFilter && ( | |||
| @@ -1,5 +1,5 @@ | |||
| import { Loader2 } from 'lucide-react'; | |||
| import { PropsWithChildren } from 'react'; | |||
| import { SkeletonCard } from './skeleton-card'; | |||
| import { TableCell, TableRow } from './ui/table'; | |||
| type IProps = { columnsLength: number }; | |||
| @@ -7,17 +7,22 @@ type IProps = { columnsLength: number }; | |||
| function Row({ children, columnsLength }: PropsWithChildren & IProps) { | |||
| return ( | |||
| <TableRow> | |||
| <TableCell colSpan={columnsLength} className="h-24 text-center "> | |||
| <TableCell colSpan={columnsLength} className="h-24 text-center"> | |||
| {children} | |||
| </TableCell> | |||
| </TableRow> | |||
| ); | |||
| } | |||
| export function TableSkeleton({ columnsLength }: { columnsLength: number }) { | |||
| export function TableSkeleton({ | |||
| columnsLength, | |||
| children, | |||
| }: PropsWithChildren & IProps) { | |||
| return ( | |||
| <Row columnsLength={columnsLength}> | |||
| <SkeletonCard></SkeletonCard> | |||
| {children || ( | |||
| <Loader2 className="animate-spin size-16 inline-block text-gray-400" /> | |||
| )} | |||
| </Row> | |||
| ); | |||
| } | |||
| @@ -99,14 +99,15 @@ export function AsyncTreeSelect({ | |||
| <li | |||
| key={x.id} | |||
| onClick={handleNodeClick(x.id)} | |||
| className="cursor-pointer hover:bg-slate-50 " | |||
| className="cursor-pointer " | |||
| > | |||
| <div className={cn('flex justify-between items-center')}> | |||
| <span | |||
| className={cn({ 'bg-cyan-50': value === x.id }, 'flex-1')} | |||
| > | |||
| {x.title} | |||
| </span> | |||
| <div | |||
| className={cn( | |||
| 'flex justify-between items-center hover:bg-accent py-0.5 px-1 rounded-md ', | |||
| { 'bg-cyan-50': value === x.id }, | |||
| )} | |||
| > | |||
| <span className={cn('flex-1 ')}>{x.title}</span> | |||
| {x.isLeaf || ( | |||
| <Button | |||
| variant={'ghost'} | |||
| @@ -146,10 +147,10 @@ export function AsyncTreeSelect({ | |||
| {selectedTitle || ( | |||
| <span className="text-slate-400">{t('common.pleaseSelect')}</span> | |||
| )} | |||
| <ChevronDown className="size-5" /> | |||
| <ChevronDown className="size-5 " /> | |||
| </div> | |||
| </PopoverTrigger> | |||
| <PopoverContent className="p-1"> | |||
| <PopoverContent className="p-1 min-w-[var(--radix-popover-trigger-width)]"> | |||
| <ul>{renderNodes()}</ul> | |||
| </PopoverContent> | |||
| </Popover> | |||
| @@ -7,70 +7,100 @@ import { | |||
| PaginationNext, | |||
| PaginationPrevious, | |||
| } from '@/components/ui/pagination'; | |||
| import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| export type DatasetsPaginationType = { | |||
| export type RAGFlowPaginationType = { | |||
| showQuickJumper?: boolean; | |||
| onChange?(page: number, pageSize?: number): void; | |||
| onChange?(page: number, pageSize: number): void; | |||
| total?: number; | |||
| current?: number; | |||
| pageSize?: number; | |||
| showSizeChanger?: boolean; | |||
| }; | |||
| export function DatasetsPagination({ | |||
| export function RAGFlowPagination({ | |||
| current = 1, | |||
| pageSize = 10, | |||
| total = 0, | |||
| onChange, | |||
| }: DatasetsPaginationType) { | |||
| showSizeChanger = true, | |||
| }: RAGFlowPaginationType) { | |||
| const [currentPage, setCurrentPage] = useState(1); | |||
| const [currentPageSize, setCurrentPageSize] = useState('10'); | |||
| const sizeChangerOptions: RAGFlowSelectOptionType[] = useMemo(() => { | |||
| return [10, 20, 50, 100].map((x) => ({ | |||
| label: <span>{x} / page</span>, | |||
| value: x.toString(), | |||
| })); | |||
| }, []); | |||
| const pages = useMemo(() => { | |||
| const num = Math.ceil(total / pageSize); | |||
| console.log('🚀 ~ pages ~ num:', num); | |||
| return new Array(num).fill(0).map((_, idx) => idx + 1); | |||
| }, [pageSize, total]); | |||
| const changePage = useCallback( | |||
| (page: number) => { | |||
| onChange?.(page, Number(currentPageSize)); | |||
| }, | |||
| [currentPageSize, onChange], | |||
| ); | |||
| const handlePreviousPageChange = useCallback(() => { | |||
| setCurrentPage((page) => { | |||
| const previousPage = page - 1; | |||
| if (previousPage > 0) { | |||
| changePage(previousPage); | |||
| return previousPage; | |||
| } | |||
| changePage(page); | |||
| return page; | |||
| }); | |||
| }, []); | |||
| }, [changePage]); | |||
| const handlePageChange = useCallback( | |||
| (page: number) => () => { | |||
| changePage(page); | |||
| setCurrentPage(page); | |||
| }, | |||
| [], | |||
| [changePage], | |||
| ); | |||
| const handleNextPageChange = useCallback(() => { | |||
| setCurrentPage((page) => { | |||
| const nextPage = page + 1; | |||
| if (nextPage <= pages.length) { | |||
| changePage(nextPage); | |||
| return nextPage; | |||
| } | |||
| changePage(page); | |||
| return page; | |||
| }); | |||
| }, [pages.length]); | |||
| }, [changePage, pages.length]); | |||
| const handlePageSizeChange = useCallback( | |||
| (size: string) => { | |||
| onChange?.(currentPage, Number(size)); | |||
| setCurrentPageSize(size); | |||
| }, | |||
| [currentPage, onChange], | |||
| ); | |||
| useEffect(() => { | |||
| setCurrentPage(current); | |||
| }, [current]); | |||
| useEffect(() => { | |||
| onChange?.(currentPage); | |||
| }, [currentPage, onChange]); | |||
| setCurrentPageSize(pageSize.toString()); | |||
| }, [pageSize]); | |||
| return ( | |||
| <section className="flex items-center justify-end"> | |||
| <section className="flex items-center justify-end"> | |||
| <span className="mr-4">Total {total}</span> | |||
| <Pagination className="w-auto mx-0"> | |||
| <Pagination className="w-auto mx-0 mr-4"> | |||
| <PaginationContent> | |||
| <PaginationItem> | |||
| <PaginationPrevious onClick={handlePreviousPageChange} /> | |||
| @@ -78,7 +108,7 @@ export function DatasetsPagination({ | |||
| {pages.map((x) => ( | |||
| <PaginationItem | |||
| key={x} | |||
| className={cn({ ['bg-red-500']: currentPage === x })} | |||
| className={cn({ ['bg-accent rounded-md']: currentPage === x })} | |||
| > | |||
| <PaginationLink onClick={handlePageChange(x)}>{x}</PaginationLink> | |||
| </PaginationItem> | |||
| @@ -91,6 +121,13 @@ export function DatasetsPagination({ | |||
| </PaginationItem> | |||
| </PaginationContent> | |||
| </Pagination> | |||
| {showSizeChanger && ( | |||
| <RAGFlowSelect | |||
| options={sizeChangerOptions} | |||
| value={currentPageSize} | |||
| onChange={handlePageSizeChange} | |||
| ></RAGFlowSelect> | |||
| )} | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef< | |||
| ref={ref} | |||
| sideOffset={sideOffset} | |||
| className={cn( | |||
| 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', | |||
| 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-w-[20vw]', | |||
| className, | |||
| )} | |||
| {...props} | |||
| @@ -1,14 +1,19 @@ | |||
| import { RowSelectionState } from '@tanstack/react-table'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useMemo, useState } from 'react'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| export function useRowSelection() { | |||
| const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); | |||
| const clearRowSelection = useCallback(() => { | |||
| setRowSelection({}); | |||
| }, []); | |||
| return { | |||
| rowSelection, | |||
| setRowSelection, | |||
| rowSelectionIsEmpty: isEmpty(rowSelection), | |||
| clearRowSelection, | |||
| }; | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| import { IFlow } from '@/interfaces/database/flow'; | |||
| import flowService from '@/services/flow-service'; | |||
| import { useQuery } from '@tanstack/react-query'; | |||
| export const enum AgentApiAction { | |||
| FetchAgentList = 'fetchAgentList', | |||
| } | |||
| export const useFetchAgentList = () => { | |||
| const { data, isFetching: loading } = useQuery<IFlow[]>({ | |||
| queryKey: [AgentApiAction.FetchAgentList], | |||
| initialData: [], | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| const { data } = await flowService.listCanvas(); | |||
| return data?.data ?? []; | |||
| }, | |||
| }); | |||
| return { data, loading }; | |||
| }; | |||
| @@ -26,7 +26,7 @@ export interface IOperatorNode { | |||
| } | |||
| export declare interface IFlow { | |||
| avatar?: null | string; | |||
| avatar?: string; | |||
| canvas_type: null; | |||
| create_date: string; | |||
| create_time: number; | |||
| @@ -15,7 +15,8 @@ import * as React from 'react'; | |||
| import { ChunkMethodDialog } from '@/components/chunk-method-dialog'; | |||
| import { RenameDialog } from '@/components/rename-dialog'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { TableSkeleton } from '@/components/table-skeleton'; | |||
| import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; | |||
| import { | |||
| Table, | |||
| TableBody, | |||
| @@ -27,6 +28,7 @@ import { | |||
| import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection'; | |||
| import { useFetchDocumentList } from '@/hooks/use-document-request'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| import { pick } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import { SetMetaDialog } from './set-meta-dialog'; | |||
| import { useChangeDocumentParser } from './use-change-document-parser'; | |||
| @@ -36,7 +38,7 @@ import { useSaveMeta } from './use-save-meta'; | |||
| export type DatasetTableProps = Pick< | |||
| ReturnType<typeof useFetchDocumentList>, | |||
| 'documents' | 'setPagination' | 'pagination' | |||
| 'documents' | 'setPagination' | 'pagination' | 'loading' | |||
| > & | |||
| Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'>; | |||
| @@ -46,6 +48,7 @@ export function DatasetTable({ | |||
| setPagination, | |||
| rowSelection, | |||
| setRowSelection, | |||
| loading, | |||
| }: DatasetTableProps) { | |||
| const [sorting, setSorting] = React.useState<SortingState>([]); | |||
| const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( | |||
| @@ -105,20 +108,6 @@ export function DatasetTable({ | |||
| 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, | |||
| @@ -152,8 +141,10 @@ export function DatasetTable({ | |||
| </TableRow> | |||
| ))} | |||
| </TableHeader> | |||
| <TableBody> | |||
| {table.getRowModel().rows?.length ? ( | |||
| <TableBody className="relative"> | |||
| {loading ? ( | |||
| <TableSkeleton columnsLength={columns.length}></TableSkeleton> | |||
| ) : table.getRowModel().rows?.length ? ( | |||
| table.getRowModel().rows.map((row) => ( | |||
| <TableRow | |||
| key={row.id} | |||
| @@ -191,22 +182,13 @@ export function DatasetTable({ | |||
| {pagination?.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> | |||
| <RAGFlowPagination | |||
| {...pick(pagination, 'current', 'pageSize')} | |||
| total={pagination.total} | |||
| onChange={(page, pageSize) => { | |||
| setPagination({ page, pageSize }); | |||
| }} | |||
| ></RAGFlowPagination> | |||
| </div> | |||
| </div> | |||
| {changeParserVisible && ( | |||
| @@ -38,6 +38,7 @@ export default function Dataset() { | |||
| setPagination, | |||
| filterValue, | |||
| handleFilterSubmit, | |||
| loading, | |||
| } = useFetchDocumentList(); | |||
| const { filters } = useSelectDatasetFilters(); | |||
| @@ -93,6 +94,7 @@ export default function Dataset() { | |||
| setPagination={setPagination} | |||
| rowSelection={rowSelection} | |||
| setRowSelection={setRowSelection} | |||
| loading={loading} | |||
| ></DatasetTable> | |||
| {documentUploadVisible && ( | |||
| <FileUploadDialog | |||
| @@ -35,16 +35,6 @@ export function useDatasetTableColumns({ | |||
| keyPrefix: 'knowledgeDetails', | |||
| }); | |||
| // const onShowRenameModal = (record: IDocumentInfo) => { | |||
| // setCurrentRecord(record); | |||
| // showRenameModal(); | |||
| // }; | |||
| // const onShowSetMetaModal = useCallback(() => { | |||
| // setRecord(); | |||
| // showSetMetaModal(); | |||
| // }, [setRecord, showSetMetaModal]); | |||
| const { navigateToChunkParsedResult } = useNavigatePage(); | |||
| const { setDocumentStatus } = useSetDocumentStatus(); | |||
| @@ -31,7 +31,7 @@ export function SideBar() { | |||
| <AvatarFallback className="rounded-lg">CN</AvatarFallback> | |||
| </Avatar> | |||
| <h3 className="text-lg font-semibold mb-2">{data.name}</h3> | |||
| <h3 className="text-lg font-semibold mb-2 line-clamp-1">{data.name}</h3> | |||
| <div className="text-sm opacity-80"> | |||
| {data.doc_num} files | {data.chunk_num} chunks | |||
| </div> | |||
| @@ -26,17 +26,21 @@ export function DatasetCard({ | |||
| return ( | |||
| <Card | |||
| key={dataset.id} | |||
| className="bg-colors-background-inverse-weak flex-1" | |||
| className="bg-colors-background-inverse-weak w-40" | |||
| onClick={navigateToDataset(dataset.id)} | |||
| > | |||
| <CardContent className="p-4"> | |||
| <section className="flex justify-between mb-4"> | |||
| <div className="flex gap-2"> | |||
| <Avatar className="w-[70px] h-[70px] rounded-lg"> | |||
| <CardContent className="p-2.5 pt-1"> | |||
| <section className="flex justify-between mb-2"> | |||
| <div className="flex gap-2 items-center"> | |||
| <Avatar className="size-6 rounded-lg"> | |||
| <AvatarImage src={dataset.avatar} /> | |||
| <AvatarFallback className="rounded-lg">CN</AvatarFallback> | |||
| <AvatarFallback className="rounded-lg ">CN</AvatarFallback> | |||
| </Avatar> | |||
| {owner && <Badge className="h-5">{owner}</Badge>} | |||
| {owner && ( | |||
| <Badge className="h-5 rounded-sm px-1 bg-background-badge text-text-badge"> | |||
| {owner} | |||
| </Badge> | |||
| )} | |||
| </div> | |||
| <DatasetDropdown | |||
| showDatasetRenameModal={showDatasetRenameModal} | |||
| @@ -48,13 +52,13 @@ export function DatasetCard({ | |||
| </DatasetDropdown> | |||
| </section> | |||
| <div className="flex justify-between items-end"> | |||
| <div> | |||
| <div className="w-full"> | |||
| <h3 className="text-lg font-semibold mb-2 line-clamp-1"> | |||
| {dataset.name} | |||
| </h3> | |||
| <p className="text-sm opacity-80">{dataset.doc_num} files</p> | |||
| <p className="text-sm opacity-80"> | |||
| Created {formatDate(dataset.update_time)} | |||
| <p className="text-xs opacity-80">{dataset.doc_num} files</p> | |||
| <p className="text-xs opacity-80"> | |||
| {formatDate(dataset.update_time)} | |||
| </p> | |||
| </div> | |||
| </div> | |||
| @@ -1,6 +1,7 @@ | |||
| import ListFilterBar from '@/components/list-filter-bar'; | |||
| import { RenameDialog } from '@/components/rename-dialog'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; | |||
| import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; | |||
| import { pick } from 'lodash'; | |||
| import { Plus } from 'lucide-react'; | |||
| @@ -8,7 +9,6 @@ import { useCallback } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { DatasetCard } from './dataset-card'; | |||
| import { DatasetCreatingDialog } from './dataset-creating-dialog'; | |||
| import { DatasetsPagination } from './datasets-pagination'; | |||
| import { useSaveKnowledge } from './hooks'; | |||
| import { useRenameDataset } from './use-rename-dataset'; | |||
| import { useSelectOwners } from './use-select-owners'; | |||
| @@ -53,7 +53,7 @@ export default function Datasets() { | |||
| ); | |||
| return ( | |||
| <section className="p-8 text-foreground"> | |||
| <section className="py-8 text-foreground"> | |||
| <ListFilterBar | |||
| title="Datasets" | |||
| searchString={searchString} | |||
| @@ -61,13 +61,14 @@ export default function Datasets() { | |||
| value={filterValue} | |||
| filters={owners} | |||
| onChange={handleFilterSubmit} | |||
| className="px-8" | |||
| > | |||
| <Button variant={'tertiary'} size={'sm'} onClick={showModal}> | |||
| <Plus className="mr-2 h-4 w-4" /> | |||
| {t('knowledgeList.createKnowledgeBase')} | |||
| </Button> | |||
| </ListFilterBar> | |||
| <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8"> | |||
| <div className="flex flex-wrap gap-4 max-h-[78vh] overflow-auto px-8"> | |||
| {kbs.map((dataset) => { | |||
| return ( | |||
| <DatasetCard | |||
| @@ -78,12 +79,12 @@ export default function Datasets() { | |||
| ); | |||
| })} | |||
| </div> | |||
| <div className="mt-8"> | |||
| <DatasetsPagination | |||
| <div className="mt-8 px-8"> | |||
| <RAGFlowPagination | |||
| {...pick(pagination, 'current', 'pageSize')} | |||
| total={total} | |||
| onChange={handlePageChange} | |||
| ></DatasetsPagination> | |||
| ></RAGFlowPagination> | |||
| </div> | |||
| {visible && ( | |||
| <DatasetCreatingDialog | |||
| @@ -73,7 +73,7 @@ export function ActionCell({ | |||
| </Button> | |||
| <ConfirmDeleteDialog> | |||
| <Button variant="ghost" size={'icon'}> | |||
| <Trash2 /> | |||
| <Trash2 className="text-text-delete-red" /> | |||
| </Button> | |||
| </ConfirmDeleteDialog> | |||
| {isSupportedPreviewDocumentType(extension) && ( | |||
| @@ -19,6 +19,7 @@ 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 { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; | |||
| import { | |||
| Table, | |||
| TableBody, | |||
| @@ -39,6 +40,7 @@ import { cn } from '@/lib/utils'; | |||
| import { formatFileSize } from '@/utils/common-util'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| import { pick } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { ActionCell } from './action-cell'; | |||
| @@ -244,20 +246,7 @@ export function FilesTable({ | |||
| 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: { | |||
| @@ -326,23 +315,15 @@ export function FilesTable({ | |||
| {table.getFilteredSelectedRowModel().rows.length} of {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> | |||
| <RAGFlowPagination | |||
| {...pick(pagination, 'current', 'pageSize')} | |||
| total={total} | |||
| onChange={(page, pageSize) => { | |||
| setPagination({ page, pageSize }); | |||
| }} | |||
| ></RAGFlowPagination> | |||
| </div> | |||
| </div> | |||
| {connectToKnowledgeVisible && ( | |||
| @@ -50,16 +50,20 @@ export default function Files() { | |||
| handleInputChange, | |||
| } = useFetchFileList(); | |||
| const { | |||
| rowSelection, | |||
| setRowSelection, | |||
| rowSelectionIsEmpty, | |||
| clearRowSelection, | |||
| } = useRowSelection(); | |||
| const { | |||
| showMoveFileModal, | |||
| moveFileVisible, | |||
| onMoveFileOk, | |||
| hideMoveFileModal, | |||
| moveFileLoading, | |||
| } = useHandleMoveFile(); | |||
| const { rowSelection, setRowSelection, rowSelectionIsEmpty } = | |||
| useRowSelection(); | |||
| } = useHandleMoveFile({ clearRowSelection }); | |||
| const { list } = useBulkOperateFile({ | |||
| files, | |||
| @@ -69,7 +69,7 @@ function LinkToDatasetForm({ | |||
| name="knowledgeIds" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>Name</FormLabel> | |||
| <FormLabel>{t('common.name')}</FormLabel> | |||
| <FormControl> | |||
| <MultiSelect | |||
| options={options} | |||
| @@ -28,7 +28,7 @@ export function useBulkOperateFile({ | |||
| label: t('common.move'), | |||
| icon: <FolderInput />, | |||
| onClick: () => { | |||
| showMoveFileModal(selectedIds); | |||
| showMoveFileModal(selectedIds, true); | |||
| }, | |||
| }, | |||
| { | |||
| @@ -1,8 +1,11 @@ | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection'; | |||
| import { useMoveFile } from '@/hooks/use-file-request'; | |||
| import { useCallback, useState } from 'react'; | |||
| import { useCallback, useRef, useState } from 'react'; | |||
| export const useHandleMoveFile = () => { | |||
| export const useHandleMoveFile = ({ | |||
| clearRowSelection, | |||
| }: Pick<UseRowSelectionType, 'clearRowSelection'>) => { | |||
| const { | |||
| visible: moveFileVisible, | |||
| hideModal: hideMoveFileModal, | |||
| @@ -10,6 +13,7 @@ export const useHandleMoveFile = () => { | |||
| } = useSetModalState(); | |||
| const { moveFile, loading } = useMoveFile(); | |||
| const [sourceFileIds, setSourceFileIds] = useState<string[]>([]); | |||
| const isBulkRef = useRef(false); | |||
| const onMoveFileOk = useCallback( | |||
| async (targetFolderId: string) => { | |||
| @@ -19,16 +23,19 @@ export const useHandleMoveFile = () => { | |||
| }); | |||
| if (ret === 0) { | |||
| // setSelectedRowKeys([]); | |||
| if (isBulkRef.current) { | |||
| clearRowSelection(); | |||
| } | |||
| hideMoveFileModal(); | |||
| } | |||
| return ret; | |||
| }, | |||
| [moveFile, hideMoveFileModal, sourceFileIds], | |||
| [moveFile, sourceFileIds, hideMoveFileModal, clearRowSelection], | |||
| ); | |||
| const handleShowMoveFileModal = useCallback( | |||
| (ids: string[]) => { | |||
| (ids: string[], isBulk = false) => { | |||
| isBulkRef.current = isBulk; | |||
| setSourceFileIds(ids); | |||
| showMoveFileModal(); | |||
| }, | |||
| @@ -0,0 +1,10 @@ | |||
| import { useFetchAgentList } from '@/hooks/use-agent-request'; | |||
| import { ApplicationCard } from './application-card'; | |||
| export function Agents() { | |||
| const { data } = useFetchAgentList(); | |||
| return data | |||
| .slice(0, 10) | |||
| .map((x) => <ApplicationCard key={x.id} app={x}></ApplicationCard>); | |||
| } | |||
| @@ -0,0 +1,30 @@ | |||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { formatDate } from '@/utils/date'; | |||
| type ApplicationCardProps = { | |||
| app: { | |||
| avatar?: string; | |||
| title: string; | |||
| update_time: number; | |||
| }; | |||
| }; | |||
| export function ApplicationCard({ app }: ApplicationCardProps) { | |||
| return ( | |||
| <Card className="bg-colors-background-inverse-weak border-colors-outline-neutral-standard w-64"> | |||
| <CardContent className="p-4 flex items-center gap-6"> | |||
| <Avatar className="size-14 rounded-lg"> | |||
| <AvatarImage src={app.avatar === null ? '' : app.avatar} /> | |||
| <AvatarFallback className="rounded-lg">CN</AvatarFallback> | |||
| </Avatar> | |||
| <div className="flex-1"> | |||
| <h3 className="text-lg font-semibold line-clamp-1 mb-1"> | |||
| {app.title} | |||
| </h3> | |||
| <p className="text-sm opacity-80">{formatDate(app.update_time)}</p> | |||
| </div> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| } | |||
| @@ -1,62 +1,58 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { Segmented, SegmentedValue } from '@/components/ui/segmented'; | |||
| import { ChevronRight, Cpu, MessageSquare, Search } from 'lucide-react'; | |||
| import { Routes } from '@/routes'; | |||
| import { Cpu, MessageSquare, Search } from 'lucide-react'; | |||
| import { useMemo, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { Agents } from './agent-list'; | |||
| import { ApplicationCard } from './application-card'; | |||
| const applications = [ | |||
| { | |||
| id: 1, | |||
| title: 'Jarvis chatbot', | |||
| type: 'Chat app', | |||
| date: '11/24/2024', | |||
| icon: <MessageSquare className="h-6 w-6" />, | |||
| update_time: '11/24/2024', | |||
| avatar: <MessageSquare className="h-6 w-6" />, | |||
| }, | |||
| { | |||
| id: 2, | |||
| title: 'Search app 01', | |||
| type: 'Search app', | |||
| date: '11/24/2024', | |||
| icon: <Search className="h-6 w-6" />, | |||
| update_time: '11/24/2024', | |||
| avatar: <Search className="h-6 w-6" />, | |||
| }, | |||
| { | |||
| id: 3, | |||
| title: 'Chatbot 01', | |||
| type: 'Chat app', | |||
| date: '11/24/2024', | |||
| icon: <MessageSquare className="h-6 w-6" />, | |||
| update_time: '11/24/2024', | |||
| avatar: <MessageSquare className="h-6 w-6" />, | |||
| }, | |||
| { | |||
| id: 4, | |||
| title: 'Workflow 01', | |||
| type: 'Agent', | |||
| date: '11/24/2024', | |||
| icon: <Cpu className="h-6 w-6" />, | |||
| update_time: '11/24/2024', | |||
| avatar: <Cpu className="h-6 w-6" />, | |||
| }, | |||
| ]; | |||
| export function Applications() { | |||
| const [val, setVal] = useState('all'); | |||
| const options = useMemo(() => { | |||
| return [ | |||
| const { t } = useTranslation(); | |||
| const options = useMemo( | |||
| () => [ | |||
| { | |||
| label: 'All', | |||
| value: 'all', | |||
| }, | |||
| { | |||
| label: 'Chat', | |||
| value: 'chat', | |||
| }, | |||
| { | |||
| label: 'Search', | |||
| value: 'search', | |||
| }, | |||
| { | |||
| label: 'Agent', | |||
| value: 'agent', | |||
| }, | |||
| ]; | |||
| }, []); | |||
| { value: Routes.Chats, label: t('header.chat') }, | |||
| { value: Routes.Searches, label: t('header.search') }, | |||
| { value: Routes.Agents, label: t('header.flow') }, | |||
| ], | |||
| [t], | |||
| ); | |||
| const handleChange = (path: SegmentedValue) => { | |||
| setVal(path as string); | |||
| @@ -73,30 +69,13 @@ export function Applications() { | |||
| className="bg-colors-background-inverse-standard text-colors-text-neutral-standard" | |||
| ></Segmented> | |||
| </div> | |||
| <div className="grid grid-cols-4 gap-6"> | |||
| {[...Array(12)].map((_, i) => { | |||
| const app = applications[i % 4]; | |||
| return ( | |||
| <Card | |||
| key={i} | |||
| className="bg-colors-background-inverse-weak border-colors-outline-neutral-standard" | |||
| > | |||
| <CardContent className="p-4 flex items-center gap-6"> | |||
| <div className="w-[70px] h-[70px] rounded-xl flex items-center justify-center bg-gradient-to-br from-[#45A7FA] via-[#AE63E3] to-[#4433FF]"> | |||
| {app.icon} | |||
| </div> | |||
| <div className="flex-1"> | |||
| <h3 className="text-lg font-semibold">{app.title}</h3> | |||
| <p className="text-sm opacity-80">{app.type}</p> | |||
| <p className="text-sm opacity-80">{app.date}</p> | |||
| </div> | |||
| <Button variant="icon" size="icon"> | |||
| <ChevronRight className="h-6 w-6" /> | |||
| </Button> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| })} | |||
| <div className="flex flex-wrap gap-4"> | |||
| {val === Routes.Agents || | |||
| [...Array(12)].map((_, i) => { | |||
| const app = applications[i % 4]; | |||
| return <ApplicationCard key={i} app={app}></ApplicationCard>; | |||
| })} | |||
| {val === Routes.Agents && <Agents></Agents>} | |||
| </div> | |||
| </section> | |||
| ); | |||
| @@ -28,7 +28,7 @@ export function Datasets() { | |||
| </div> | |||
| ) : ( | |||
| <div className="flex gap-4 flex-1"> | |||
| {kbs.slice(0, 4).map((dataset) => ( | |||
| {kbs.slice(0, 6).map((dataset) => ( | |||
| <DatasetCard | |||
| key={dataset.id} | |||
| dataset={dataset} | |||
| @@ -42,6 +42,9 @@ module.exports = { | |||
| 'colors-text-inverse-weak': 'var(--colors-text-inverse-weak)', | |||
| 'text-delete-red': 'var(--text-delete-red)', | |||
| 'background-badge': 'var(--background-badge)', | |||
| 'text-badge': 'var(--text-badge)', | |||
| primary: { | |||
| DEFAULT: 'hsl(var(--primary))', | |||
| foreground: 'hsl(var(--primary-foreground))', | |||
| @@ -73,6 +73,9 @@ | |||
| --sidebar-ring: 217.2 91.2% 59.8%; | |||
| --background-inverse-strong: rgba(255, 255, 255, 0.15); | |||
| --background-badge: rgba(22, 22, 24, 0.5); | |||
| --text-badge: rgba(151, 154, 171, 1); | |||
| } | |||
| .dark { | |||
| @@ -122,6 +125,9 @@ | |||
| --background-core-weak: rgb(101, 75, 248); | |||
| --background-core-weak-foreground: rgba(255, 255, 255, 1); | |||
| --background-badge: rgba(22, 22, 24, 0.5); | |||
| --text-badge: rgba(151, 154, 171, 1); | |||
| --colors-background-core-standard: rgba(137, 126, 255, 1); | |||
| --colors-background-core-strong: rgba(152, 147, 255, 1); | |||
| --colors-background-core-weak: rgba(101, 75, 248, 1); | |||