Ver código fonte

Feat: Bind data to the agent module of the home page #3221 (#7385)

### 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
balibabu 6 meses atrás
pai
commit
5bb1c383ac
Nenhuma conta vinculada ao e-mail do autor do commit

+ 6
- 2
web/src/components/list-filter-bar/index.tsx Ver arquivo

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 && (

+ 9
- 4
web/src/components/table-skeleton.tsx Ver arquivo

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>
); );
} }

+ 10
- 9
web/src/components/ui/async-tree-select.tsx Ver arquivo

<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>

web/src/pages/datasets/datasets-pagination.tsx → web/src/components/ui/ragflow-pagination.tsx Ver arquivo

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>
); );
} }

+ 1
- 1
web/src/components/ui/tooltip.tsx Ver arquivo

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}

+ 6
- 1
web/src/hooks/logic-hooks/use-row-selection.ts Ver arquivo

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,
}; };
} }



+ 22
- 0
web/src/hooks/use-agent-request.ts Ver arquivo

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 };
};

+ 1
- 1
web/src/interfaces/database/flow.ts Ver arquivo

} }


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;

+ 16
- 34
web/src/pages/dataset/dataset/dataset-table.tsx Ver arquivo



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 && (

+ 2
- 0
web/src/pages/dataset/dataset/index.tsx Ver arquivo

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

+ 0
- 10
web/src/pages/dataset/dataset/use-dataset-table-columns.tsx Ver arquivo

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();



+ 1
- 1
web/src/pages/dataset/sidebar/index.tsx Ver arquivo

<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>

+ 15
- 11
web/src/pages/datasets/dataset-card.tsx Ver arquivo

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>

+ 7
- 6
web/src/pages/datasets/index.tsx Ver arquivo

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

+ 1
- 1
web/src/pages/files/action-cell.tsx Ver arquivo

</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) && (

+ 11
- 30
web/src/pages/files/files-table.tsx Ver arquivo

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 && (

+ 8
- 4
web/src/pages/files/index.tsx Ver arquivo

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,

+ 1
- 1
web/src/pages/files/link-to-dataset-dialog.tsx Ver arquivo

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}

+ 1
- 1
web/src/pages/files/use-bulk-operate-file.tsx Ver arquivo

label: t('common.move'), label: t('common.move'),
icon: <FolderInput />, icon: <FolderInput />,
onClick: () => { onClick: () => {
showMoveFileModal(selectedIds);
showMoveFileModal(selectedIds, true);
}, },
}, },
{ {

+ 12
- 5
web/src/pages/files/use-move-file.ts Ver arquivo

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();
}, },

+ 10
- 0
web/src/pages/home/agent-list.tsx Ver arquivo

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>);
}

+ 30
- 0
web/src/pages/home/application-card.tsx Ver arquivo

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>
);
}

+ 30
- 51
web/src/pages/home/applications.tsx Ver arquivo

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>
); );

+ 1
- 1
web/src/pages/home/datasets.tsx Ver arquivo

</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}

+ 3
- 0
web/tailwind.config.js Ver arquivo

'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))',

+ 6
- 0
web/tailwind.css Ver arquivo

--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);

Carregando…
Cancelar
Salvar