### 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
| import { cn } from '@/lib/utils'; | |||||
| import { ChevronDown } from 'lucide-react'; | import { ChevronDown } from 'lucide-react'; | ||||
| import React, { | import React, { | ||||
| ChangeEventHandler, | ChangeEventHandler, | ||||
| value, | value, | ||||
| onChange, | onChange, | ||||
| filters, | filters, | ||||
| }: PropsWithChildren<IProps & Omit<CheckboxFormMultipleProps, 'setOpen'>>) { | |||||
| className, | |||||
| }: PropsWithChildren<IProps & Omit<CheckboxFormMultipleProps, 'setOpen'>> & { | |||||
| className?: string; | |||||
| }) { | |||||
| const filterCount = useMemo(() => { | const filterCount = useMemo(() => { | ||||
| return typeof value === 'object' && value !== null | return typeof value === 'object' && value !== null | ||||
| ? Object.values(value).reduce((pre, cur) => { | ? Object.values(value).reduce((pre, cur) => { | ||||
| }, [value]); | }, [value]); | ||||
| return ( | 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> | <span className="text-3xl font-bold ">{leftPanel || title}</span> | ||||
| <div className="flex gap-4 items-center"> | <div className="flex gap-4 items-center"> | ||||
| {showFilter && ( | {showFilter && ( |
| import { Loader2 } from 'lucide-react'; | |||||
| import { PropsWithChildren } from 'react'; | import { PropsWithChildren } from 'react'; | ||||
| import { SkeletonCard } from './skeleton-card'; | |||||
| import { TableCell, TableRow } from './ui/table'; | import { TableCell, TableRow } from './ui/table'; | ||||
| type IProps = { columnsLength: number }; | type IProps = { columnsLength: number }; | ||||
| function Row({ children, columnsLength }: PropsWithChildren & IProps) { | function Row({ children, columnsLength }: PropsWithChildren & IProps) { | ||||
| return ( | return ( | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={columnsLength} className="h-24 text-center "> | |||||
| <TableCell colSpan={columnsLength} className="h-24 text-center"> | |||||
| {children} | {children} | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| ); | ); | ||||
| } | } | ||||
| export function TableSkeleton({ columnsLength }: { columnsLength: number }) { | |||||
| export function TableSkeleton({ | |||||
| columnsLength, | |||||
| children, | |||||
| }: PropsWithChildren & IProps) { | |||||
| return ( | return ( | ||||
| <Row columnsLength={columnsLength}> | <Row columnsLength={columnsLength}> | ||||
| <SkeletonCard></SkeletonCard> | |||||
| {children || ( | |||||
| <Loader2 className="animate-spin size-16 inline-block text-gray-400" /> | |||||
| )} | |||||
| </Row> | </Row> | ||||
| ); | ); | ||||
| } | } |
| <li | <li | ||||
| key={x.id} | key={x.id} | ||||
| onClick={handleNodeClick(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 || ( | {x.isLeaf || ( | ||||
| <Button | <Button | ||||
| variant={'ghost'} | variant={'ghost'} | ||||
| {selectedTitle || ( | {selectedTitle || ( | ||||
| <span className="text-slate-400">{t('common.pleaseSelect')}</span> | <span className="text-slate-400">{t('common.pleaseSelect')}</span> | ||||
| )} | )} | ||||
| <ChevronDown className="size-5" /> | |||||
| <ChevronDown className="size-5 " /> | |||||
| </div> | </div> | ||||
| </PopoverTrigger> | </PopoverTrigger> | ||||
| <PopoverContent className="p-1"> | |||||
| <PopoverContent className="p-1 min-w-[var(--radix-popover-trigger-width)]"> | |||||
| <ul>{renderNodes()}</ul> | <ul>{renderNodes()}</ul> | ||||
| </PopoverContent> | </PopoverContent> | ||||
| </Popover> | </Popover> |
| PaginationNext, | PaginationNext, | ||||
| PaginationPrevious, | PaginationPrevious, | ||||
| } from '@/components/ui/pagination'; | } from '@/components/ui/pagination'; | ||||
| import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select'; | |||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | import { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| export type DatasetsPaginationType = { | |||||
| export type RAGFlowPaginationType = { | |||||
| showQuickJumper?: boolean; | showQuickJumper?: boolean; | ||||
| onChange?(page: number, pageSize?: number): void; | |||||
| onChange?(page: number, pageSize: number): void; | |||||
| total?: number; | total?: number; | ||||
| current?: number; | current?: number; | ||||
| pageSize?: number; | pageSize?: number; | ||||
| showSizeChanger?: boolean; | |||||
| }; | }; | ||||
| export function DatasetsPagination({ | |||||
| export function RAGFlowPagination({ | |||||
| current = 1, | current = 1, | ||||
| pageSize = 10, | pageSize = 10, | ||||
| total = 0, | total = 0, | ||||
| onChange, | onChange, | ||||
| }: DatasetsPaginationType) { | |||||
| showSizeChanger = true, | |||||
| }: RAGFlowPaginationType) { | |||||
| const [currentPage, setCurrentPage] = useState(1); | 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 pages = useMemo(() => { | ||||
| const num = Math.ceil(total / pageSize); | const num = Math.ceil(total / pageSize); | ||||
| console.log('🚀 ~ pages ~ num:', num); | |||||
| return new Array(num).fill(0).map((_, idx) => idx + 1); | return new Array(num).fill(0).map((_, idx) => idx + 1); | ||||
| }, [pageSize, total]); | }, [pageSize, total]); | ||||
| const changePage = useCallback( | |||||
| (page: number) => { | |||||
| onChange?.(page, Number(currentPageSize)); | |||||
| }, | |||||
| [currentPageSize, onChange], | |||||
| ); | |||||
| const handlePreviousPageChange = useCallback(() => { | const handlePreviousPageChange = useCallback(() => { | ||||
| setCurrentPage((page) => { | setCurrentPage((page) => { | ||||
| const previousPage = page - 1; | const previousPage = page - 1; | ||||
| if (previousPage > 0) { | if (previousPage > 0) { | ||||
| changePage(previousPage); | |||||
| return previousPage; | return previousPage; | ||||
| } | } | ||||
| changePage(page); | |||||
| return page; | return page; | ||||
| }); | }); | ||||
| }, []); | |||||
| }, [changePage]); | |||||
| const handlePageChange = useCallback( | const handlePageChange = useCallback( | ||||
| (page: number) => () => { | (page: number) => () => { | ||||
| changePage(page); | |||||
| setCurrentPage(page); | setCurrentPage(page); | ||||
| }, | }, | ||||
| [], | |||||
| [changePage], | |||||
| ); | ); | ||||
| const handleNextPageChange = useCallback(() => { | const handleNextPageChange = useCallback(() => { | ||||
| setCurrentPage((page) => { | setCurrentPage((page) => { | ||||
| const nextPage = page + 1; | const nextPage = page + 1; | ||||
| if (nextPage <= pages.length) { | if (nextPage <= pages.length) { | ||||
| changePage(nextPage); | |||||
| return nextPage; | return nextPage; | ||||
| } | } | ||||
| changePage(page); | |||||
| return page; | return page; | ||||
| }); | }); | ||||
| }, [pages.length]); | |||||
| }, [changePage, pages.length]); | |||||
| const handlePageSizeChange = useCallback( | |||||
| (size: string) => { | |||||
| onChange?.(currentPage, Number(size)); | |||||
| setCurrentPageSize(size); | |||||
| }, | |||||
| [currentPage, onChange], | |||||
| ); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setCurrentPage(current); | setCurrentPage(current); | ||||
| }, [current]); | }, [current]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| onChange?.(currentPage); | |||||
| }, [currentPage, onChange]); | |||||
| setCurrentPageSize(pageSize.toString()); | |||||
| }, [pageSize]); | |||||
| return ( | return ( | ||||
| <section className="flex items-center justify-end"> | |||||
| <section className="flex items-center justify-end"> | |||||
| <span className="mr-4">Total {total}</span> | <span className="mr-4">Total {total}</span> | ||||
| <Pagination className="w-auto mx-0"> | |||||
| <Pagination className="w-auto mx-0 mr-4"> | |||||
| <PaginationContent> | <PaginationContent> | ||||
| <PaginationItem> | <PaginationItem> | ||||
| <PaginationPrevious onClick={handlePreviousPageChange} /> | <PaginationPrevious onClick={handlePreviousPageChange} /> | ||||
| {pages.map((x) => ( | {pages.map((x) => ( | ||||
| <PaginationItem | <PaginationItem | ||||
| key={x} | key={x} | ||||
| className={cn({ ['bg-red-500']: currentPage === x })} | |||||
| className={cn({ ['bg-accent rounded-md']: currentPage === x })} | |||||
| > | > | ||||
| <PaginationLink onClick={handlePageChange(x)}>{x}</PaginationLink> | <PaginationLink onClick={handlePageChange(x)}>{x}</PaginationLink> | ||||
| </PaginationItem> | </PaginationItem> | ||||
| </PaginationItem> | </PaginationItem> | ||||
| </PaginationContent> | </PaginationContent> | ||||
| </Pagination> | </Pagination> | ||||
| {showSizeChanger && ( | |||||
| <RAGFlowSelect | |||||
| options={sizeChangerOptions} | |||||
| value={currentPageSize} | |||||
| onChange={handlePageSizeChange} | |||||
| ></RAGFlowSelect> | |||||
| )} | |||||
| </section> | </section> | ||||
| ); | ); | ||||
| } | } |
| ref={ref} | ref={ref} | ||||
| sideOffset={sideOffset} | sideOffset={sideOffset} | ||||
| className={cn( | 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, | className, | ||||
| )} | )} | ||||
| {...props} | {...props} |
| import { RowSelectionState } from '@tanstack/react-table'; | import { RowSelectionState } from '@tanstack/react-table'; | ||||
| import { isEmpty } from 'lodash'; | import { isEmpty } from 'lodash'; | ||||
| import { useMemo, useState } from 'react'; | |||||
| import { useCallback, useMemo, useState } from 'react'; | |||||
| export function useRowSelection() { | export function useRowSelection() { | ||||
| const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); | const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); | ||||
| const clearRowSelection = useCallback(() => { | |||||
| setRowSelection({}); | |||||
| }, []); | |||||
| return { | return { | ||||
| rowSelection, | rowSelection, | ||||
| setRowSelection, | setRowSelection, | ||||
| rowSelectionIsEmpty: isEmpty(rowSelection), | rowSelectionIsEmpty: isEmpty(rowSelection), | ||||
| clearRowSelection, | |||||
| }; | }; | ||||
| } | } | ||||
| 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 }; | |||||
| }; |
| } | } | ||||
| export declare interface IFlow { | export declare interface IFlow { | ||||
| avatar?: null | string; | |||||
| avatar?: string; | |||||
| canvas_type: null; | canvas_type: null; | ||||
| create_date: string; | create_date: string; | ||||
| create_time: number; | create_time: number; |
| import { ChunkMethodDialog } from '@/components/chunk-method-dialog'; | import { ChunkMethodDialog } from '@/components/chunk-method-dialog'; | ||||
| import { RenameDialog } from '@/components/rename-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 { | import { | ||||
| Table, | Table, | ||||
| TableBody, | TableBody, | ||||
| import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection'; | import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection'; | ||||
| import { useFetchDocumentList } from '@/hooks/use-document-request'; | import { useFetchDocumentList } from '@/hooks/use-document-request'; | ||||
| import { getExtension } from '@/utils/document-util'; | import { getExtension } from '@/utils/document-util'; | ||||
| import { pick } from 'lodash'; | |||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import { SetMetaDialog } from './set-meta-dialog'; | import { SetMetaDialog } from './set-meta-dialog'; | ||||
| import { useChangeDocumentParser } from './use-change-document-parser'; | import { useChangeDocumentParser } from './use-change-document-parser'; | ||||
| export type DatasetTableProps = Pick< | export type DatasetTableProps = Pick< | ||||
| ReturnType<typeof useFetchDocumentList>, | ReturnType<typeof useFetchDocumentList>, | ||||
| 'documents' | 'setPagination' | 'pagination' | |||||
| 'documents' | 'setPagination' | 'pagination' | 'loading' | |||||
| > & | > & | ||||
| Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'>; | Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'>; | ||||
| setPagination, | setPagination, | ||||
| rowSelection, | rowSelection, | ||||
| setRowSelection, | setRowSelection, | ||||
| loading, | |||||
| }: DatasetTableProps) { | }: DatasetTableProps) { | ||||
| const [sorting, setSorting] = React.useState<SortingState>([]); | const [sorting, setSorting] = React.useState<SortingState>([]); | ||||
| const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( | const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( | ||||
| getFilteredRowModel: getFilteredRowModel(), | getFilteredRowModel: getFilteredRowModel(), | ||||
| onColumnVisibilityChange: setColumnVisibility, | onColumnVisibilityChange: setColumnVisibility, | ||||
| onRowSelectionChange: setRowSelection, | 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 | manualPagination: true, //we're doing manual "server-side" pagination | ||||
| state: { | state: { | ||||
| sorting, | sorting, | ||||
| </TableRow> | </TableRow> | ||||
| ))} | ))} | ||||
| </TableHeader> | </TableHeader> | ||||
| <TableBody> | |||||
| {table.getRowModel().rows?.length ? ( | |||||
| <TableBody className="relative"> | |||||
| {loading ? ( | |||||
| <TableSkeleton columnsLength={columns.length}></TableSkeleton> | |||||
| ) : table.getRowModel().rows?.length ? ( | |||||
| table.getRowModel().rows.map((row) => ( | table.getRowModel().rows.map((row) => ( | ||||
| <TableRow | <TableRow | ||||
| key={row.id} | key={row.id} | ||||
| {pagination?.total} row(s) selected. | {pagination?.total} row(s) selected. | ||||
| </div> | </div> | ||||
| <div className="space-x-2"> | <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> | ||||
| </div> | </div> | ||||
| {changeParserVisible && ( | {changeParserVisible && ( |
| setPagination, | setPagination, | ||||
| filterValue, | filterValue, | ||||
| handleFilterSubmit, | handleFilterSubmit, | ||||
| loading, | |||||
| } = useFetchDocumentList(); | } = useFetchDocumentList(); | ||||
| const { filters } = useSelectDatasetFilters(); | const { filters } = useSelectDatasetFilters(); | ||||
| setPagination={setPagination} | setPagination={setPagination} | ||||
| rowSelection={rowSelection} | rowSelection={rowSelection} | ||||
| setRowSelection={setRowSelection} | setRowSelection={setRowSelection} | ||||
| loading={loading} | |||||
| ></DatasetTable> | ></DatasetTable> | ||||
| {documentUploadVisible && ( | {documentUploadVisible && ( | ||||
| <FileUploadDialog | <FileUploadDialog |
| keyPrefix: 'knowledgeDetails', | keyPrefix: 'knowledgeDetails', | ||||
| }); | }); | ||||
| // const onShowRenameModal = (record: IDocumentInfo) => { | |||||
| // setCurrentRecord(record); | |||||
| // showRenameModal(); | |||||
| // }; | |||||
| // const onShowSetMetaModal = useCallback(() => { | |||||
| // setRecord(); | |||||
| // showSetMetaModal(); | |||||
| // }, [setRecord, showSetMetaModal]); | |||||
| const { navigateToChunkParsedResult } = useNavigatePage(); | const { navigateToChunkParsedResult } = useNavigatePage(); | ||||
| const { setDocumentStatus } = useSetDocumentStatus(); | const { setDocumentStatus } = useSetDocumentStatus(); | ||||
| <AvatarFallback className="rounded-lg">CN</AvatarFallback> | <AvatarFallback className="rounded-lg">CN</AvatarFallback> | ||||
| </Avatar> | </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"> | <div className="text-sm opacity-80"> | ||||
| {data.doc_num} files | {data.chunk_num} chunks | {data.doc_num} files | {data.chunk_num} chunks | ||||
| </div> | </div> |
| return ( | return ( | ||||
| <Card | <Card | ||||
| key={dataset.id} | key={dataset.id} | ||||
| className="bg-colors-background-inverse-weak flex-1" | |||||
| className="bg-colors-background-inverse-weak w-40" | |||||
| onClick={navigateToDataset(dataset.id)} | 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} /> | <AvatarImage src={dataset.avatar} /> | ||||
| <AvatarFallback className="rounded-lg">CN</AvatarFallback> | |||||
| <AvatarFallback className="rounded-lg ">CN</AvatarFallback> | |||||
| </Avatar> | </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> | </div> | ||||
| <DatasetDropdown | <DatasetDropdown | ||||
| showDatasetRenameModal={showDatasetRenameModal} | showDatasetRenameModal={showDatasetRenameModal} | ||||
| </DatasetDropdown> | </DatasetDropdown> | ||||
| </section> | </section> | ||||
| <div className="flex justify-between items-end"> | <div className="flex justify-between items-end"> | ||||
| <div> | |||||
| <div className="w-full"> | |||||
| <h3 className="text-lg font-semibold mb-2 line-clamp-1"> | <h3 className="text-lg font-semibold mb-2 line-clamp-1"> | ||||
| {dataset.name} | {dataset.name} | ||||
| </h3> | </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> | </p> | ||||
| </div> | </div> | ||||
| </div> | </div> |
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { RenameDialog } from '@/components/rename-dialog'; | import { RenameDialog } from '@/components/rename-dialog'; | ||||
| import { Button } from '@/components/ui/button'; | import { Button } from '@/components/ui/button'; | ||||
| import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; | |||||
| import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; | import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; | ||||
| import { pick } from 'lodash'; | import { pick } from 'lodash'; | ||||
| import { Plus } from 'lucide-react'; | import { Plus } from 'lucide-react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { DatasetCard } from './dataset-card'; | import { DatasetCard } from './dataset-card'; | ||||
| import { DatasetCreatingDialog } from './dataset-creating-dialog'; | import { DatasetCreatingDialog } from './dataset-creating-dialog'; | ||||
| import { DatasetsPagination } from './datasets-pagination'; | |||||
| import { useSaveKnowledge } from './hooks'; | import { useSaveKnowledge } from './hooks'; | ||||
| import { useRenameDataset } from './use-rename-dataset'; | import { useRenameDataset } from './use-rename-dataset'; | ||||
| import { useSelectOwners } from './use-select-owners'; | import { useSelectOwners } from './use-select-owners'; | ||||
| ); | ); | ||||
| return ( | return ( | ||||
| <section className="p-8 text-foreground"> | |||||
| <section className="py-8 text-foreground"> | |||||
| <ListFilterBar | <ListFilterBar | ||||
| title="Datasets" | title="Datasets" | ||||
| searchString={searchString} | searchString={searchString} | ||||
| value={filterValue} | value={filterValue} | ||||
| filters={owners} | filters={owners} | ||||
| onChange={handleFilterSubmit} | onChange={handleFilterSubmit} | ||||
| className="px-8" | |||||
| > | > | ||||
| <Button variant={'tertiary'} size={'sm'} onClick={showModal}> | <Button variant={'tertiary'} size={'sm'} onClick={showModal}> | ||||
| <Plus className="mr-2 h-4 w-4" /> | <Plus className="mr-2 h-4 w-4" /> | ||||
| {t('knowledgeList.createKnowledgeBase')} | {t('knowledgeList.createKnowledgeBase')} | ||||
| </Button> | </Button> | ||||
| </ListFilterBar> | </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) => { | {kbs.map((dataset) => { | ||||
| return ( | return ( | ||||
| <DatasetCard | <DatasetCard | ||||
| ); | ); | ||||
| })} | })} | ||||
| </div> | </div> | ||||
| <div className="mt-8"> | |||||
| <DatasetsPagination | |||||
| <div className="mt-8 px-8"> | |||||
| <RAGFlowPagination | |||||
| {...pick(pagination, 'current', 'pageSize')} | {...pick(pagination, 'current', 'pageSize')} | ||||
| total={total} | total={total} | ||||
| onChange={handlePageChange} | onChange={handlePageChange} | ||||
| ></DatasetsPagination> | |||||
| ></RAGFlowPagination> | |||||
| </div> | </div> | ||||
| {visible && ( | {visible && ( | ||||
| <DatasetCreatingDialog | <DatasetCreatingDialog |
| </Button> | </Button> | ||||
| <ConfirmDeleteDialog> | <ConfirmDeleteDialog> | ||||
| <Button variant="ghost" size={'icon'}> | <Button variant="ghost" size={'icon'}> | ||||
| <Trash2 /> | |||||
| <Trash2 className="text-text-delete-red" /> | |||||
| </Button> | </Button> | ||||
| </ConfirmDeleteDialog> | </ConfirmDeleteDialog> | ||||
| {isSupportedPreviewDocumentType(extension) && ( | {isSupportedPreviewDocumentType(extension) && ( |
| import { TableEmpty, TableSkeleton } from '@/components/table-skeleton'; | import { TableEmpty, TableSkeleton } from '@/components/table-skeleton'; | ||||
| import { Button } from '@/components/ui/button'; | import { Button } from '@/components/ui/button'; | ||||
| import { Checkbox } from '@/components/ui/checkbox'; | import { Checkbox } from '@/components/ui/checkbox'; | ||||
| import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; | |||||
| import { | import { | ||||
| Table, | Table, | ||||
| TableBody, | TableBody, | ||||
| import { formatFileSize } from '@/utils/common-util'; | import { formatFileSize } from '@/utils/common-util'; | ||||
| import { formatDate } from '@/utils/date'; | import { formatDate } from '@/utils/date'; | ||||
| import { getExtension } from '@/utils/document-util'; | import { getExtension } from '@/utils/document-util'; | ||||
| import { pick } from 'lodash'; | |||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { ActionCell } from './action-cell'; | import { ActionCell } from './action-cell'; | ||||
| getFilteredRowModel: getFilteredRowModel(), | getFilteredRowModel: getFilteredRowModel(), | ||||
| onColumnVisibilityChange: setColumnVisibility, | onColumnVisibilityChange: setColumnVisibility, | ||||
| onRowSelectionChange: setRowSelection, | 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 | manualPagination: true, //we're doing manual "server-side" pagination | ||||
| state: { | state: { | ||||
| {table.getFilteredSelectedRowModel().rows.length} of {total} row(s) | {table.getFilteredSelectedRowModel().rows.length} of {total} row(s) | ||||
| selected. | selected. | ||||
| </div> | </div> | ||||
| <div className="space-x-2"> | <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> | ||||
| </div> | </div> | ||||
| {connectToKnowledgeVisible && ( | {connectToKnowledgeVisible && ( |
| handleInputChange, | handleInputChange, | ||||
| } = useFetchFileList(); | } = useFetchFileList(); | ||||
| const { | |||||
| rowSelection, | |||||
| setRowSelection, | |||||
| rowSelectionIsEmpty, | |||||
| clearRowSelection, | |||||
| } = useRowSelection(); | |||||
| const { | const { | ||||
| showMoveFileModal, | showMoveFileModal, | ||||
| moveFileVisible, | moveFileVisible, | ||||
| onMoveFileOk, | onMoveFileOk, | ||||
| hideMoveFileModal, | hideMoveFileModal, | ||||
| moveFileLoading, | moveFileLoading, | ||||
| } = useHandleMoveFile(); | |||||
| const { rowSelection, setRowSelection, rowSelectionIsEmpty } = | |||||
| useRowSelection(); | |||||
| } = useHandleMoveFile({ clearRowSelection }); | |||||
| const { list } = useBulkOperateFile({ | const { list } = useBulkOperateFile({ | ||||
| files, | files, |
| name="knowledgeIds" | name="knowledgeIds" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | <FormItem> | ||||
| <FormLabel>Name</FormLabel> | |||||
| <FormLabel>{t('common.name')}</FormLabel> | |||||
| <FormControl> | <FormControl> | ||||
| <MultiSelect | <MultiSelect | ||||
| options={options} | options={options} |
| label: t('common.move'), | label: t('common.move'), | ||||
| icon: <FolderInput />, | icon: <FolderInput />, | ||||
| onClick: () => { | onClick: () => { | ||||
| showMoveFileModal(selectedIds); | |||||
| showMoveFileModal(selectedIds, true); | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { |
| import { useSetModalState } from '@/hooks/common-hooks'; | import { useSetModalState } from '@/hooks/common-hooks'; | ||||
| import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection'; | |||||
| import { useMoveFile } from '@/hooks/use-file-request'; | 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 { | const { | ||||
| visible: moveFileVisible, | visible: moveFileVisible, | ||||
| hideModal: hideMoveFileModal, | hideModal: hideMoveFileModal, | ||||
| } = useSetModalState(); | } = useSetModalState(); | ||||
| const { moveFile, loading } = useMoveFile(); | const { moveFile, loading } = useMoveFile(); | ||||
| const [sourceFileIds, setSourceFileIds] = useState<string[]>([]); | const [sourceFileIds, setSourceFileIds] = useState<string[]>([]); | ||||
| const isBulkRef = useRef(false); | |||||
| const onMoveFileOk = useCallback( | const onMoveFileOk = useCallback( | ||||
| async (targetFolderId: string) => { | async (targetFolderId: string) => { | ||||
| }); | }); | ||||
| if (ret === 0) { | if (ret === 0) { | ||||
| // setSelectedRowKeys([]); | |||||
| if (isBulkRef.current) { | |||||
| clearRowSelection(); | |||||
| } | |||||
| hideMoveFileModal(); | hideMoveFileModal(); | ||||
| } | } | ||||
| return ret; | return ret; | ||||
| }, | }, | ||||
| [moveFile, hideMoveFileModal, sourceFileIds], | |||||
| [moveFile, sourceFileIds, hideMoveFileModal, clearRowSelection], | |||||
| ); | ); | ||||
| const handleShowMoveFileModal = useCallback( | const handleShowMoveFileModal = useCallback( | ||||
| (ids: string[]) => { | |||||
| (ids: string[], isBulk = false) => { | |||||
| isBulkRef.current = isBulk; | |||||
| setSourceFileIds(ids); | setSourceFileIds(ids); | ||||
| showMoveFileModal(); | showMoveFileModal(); | ||||
| }, | }, |
| 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>); | |||||
| } |
| 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> | |||||
| ); | |||||
| } |
| import { Button } from '@/components/ui/button'; | |||||
| import { Card, CardContent } from '@/components/ui/card'; | |||||
| import { Segmented, SegmentedValue } from '@/components/ui/segmented'; | 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 { useMemo, useState } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { Agents } from './agent-list'; | |||||
| import { ApplicationCard } from './application-card'; | |||||
| const applications = [ | const applications = [ | ||||
| { | { | ||||
| id: 1, | id: 1, | ||||
| title: 'Jarvis chatbot', | title: 'Jarvis chatbot', | ||||
| type: 'Chat app', | 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, | id: 2, | ||||
| title: 'Search app 01', | title: 'Search app 01', | ||||
| type: 'Search app', | 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, | id: 3, | ||||
| title: 'Chatbot 01', | title: 'Chatbot 01', | ||||
| type: 'Chat app', | 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, | id: 4, | ||||
| title: 'Workflow 01', | title: 'Workflow 01', | ||||
| type: 'Agent', | 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() { | export function Applications() { | ||||
| const [val, setVal] = useState('all'); | const [val, setVal] = useState('all'); | ||||
| const options = useMemo(() => { | |||||
| return [ | |||||
| const { t } = useTranslation(); | |||||
| const options = useMemo( | |||||
| () => [ | |||||
| { | { | ||||
| label: 'All', | label: 'All', | ||||
| value: '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) => { | const handleChange = (path: SegmentedValue) => { | ||||
| setVal(path as string); | setVal(path as string); | ||||
| className="bg-colors-background-inverse-standard text-colors-text-neutral-standard" | className="bg-colors-background-inverse-standard text-colors-text-neutral-standard" | ||||
| ></Segmented> | ></Segmented> | ||||
| </div> | </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> | </div> | ||||
| </section> | </section> | ||||
| ); | ); |
| </div> | </div> | ||||
| ) : ( | ) : ( | ||||
| <div className="flex gap-4 flex-1"> | <div className="flex gap-4 flex-1"> | ||||
| {kbs.slice(0, 4).map((dataset) => ( | |||||
| {kbs.slice(0, 6).map((dataset) => ( | |||||
| <DatasetCard | <DatasetCard | ||||
| key={dataset.id} | key={dataset.id} | ||||
| dataset={dataset} | dataset={dataset} |
| 'colors-text-inverse-weak': 'var(--colors-text-inverse-weak)', | 'colors-text-inverse-weak': 'var(--colors-text-inverse-weak)', | ||||
| 'text-delete-red': 'var(--text-delete-red)', | 'text-delete-red': 'var(--text-delete-red)', | ||||
| 'background-badge': 'var(--background-badge)', | |||||
| 'text-badge': 'var(--text-badge)', | |||||
| primary: { | primary: { | ||||
| DEFAULT: 'hsl(var(--primary))', | DEFAULT: 'hsl(var(--primary))', | ||||
| foreground: 'hsl(var(--primary-foreground))', | foreground: 'hsl(var(--primary-foreground))', |
| --sidebar-ring: 217.2 91.2% 59.8%; | --sidebar-ring: 217.2 91.2% 59.8%; | ||||
| --background-inverse-strong: rgba(255, 255, 255, 0.15); | --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 { | .dark { | ||||
| --background-core-weak: rgb(101, 75, 248); | --background-core-weak: rgb(101, 75, 248); | ||||
| --background-core-weak-foreground: rgba(255, 255, 255, 1); | --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-standard: rgba(137, 126, 255, 1); | ||||
| --colors-background-core-strong: rgba(152, 147, 255, 1); | --colors-background-core-strong: rgba(152, 147, 255, 1); | ||||
| --colors-background-core-weak: rgba(101, 75, 248, 1); | --colors-background-core-weak: rgba(101, 75, 248, 1); |