feat: add ragflow web project & add pnpm workspace file
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
export enum LogTabs {
|
||||
FILE_LOGS = 'fileLogs',
|
||||
DATASET_LOGS = 'datasetLogs',
|
||||
}
|
||||
|
||||
export enum ProcessingType {
|
||||
knowledgeGraph = 'GraphRAG',
|
||||
raptor = 'RAPTOR',
|
||||
}
|
||||
|
||||
export const ProcessingTypeMap = {
|
||||
[ProcessingType.knowledgeGraph]: 'Knowledge Graph',
|
||||
[ProcessingType.raptor]: 'RAPTOR',
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { FilterButton } from '@/components/list-filter-bar';
|
||||
import {
|
||||
CheckboxFormMultipleProps,
|
||||
FilterPopover,
|
||||
} from '@/components/list-filter-bar/filter-popover';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SearchInput } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChangeEventHandler, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LogTabs } from './dataset-common';
|
||||
|
||||
interface IProps {
|
||||
searchString?: string;
|
||||
onSearchChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
active?: (typeof LogTabs)[keyof typeof LogTabs];
|
||||
setActive?: (active: (typeof LogTabs)[keyof typeof LogTabs]) => void;
|
||||
}
|
||||
const DatasetFilter = (
|
||||
props: IProps & Omit<CheckboxFormMultipleProps, 'setOpen'>,
|
||||
) => {
|
||||
const {
|
||||
searchString,
|
||||
onSearchChange,
|
||||
value,
|
||||
onChange,
|
||||
filters,
|
||||
onOpenChange,
|
||||
active = LogTabs.FILE_LOGS,
|
||||
setActive,
|
||||
...rest
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const filterCount = useMemo(() => {
|
||||
return typeof value === 'object' && value !== null
|
||||
? Object.values(value).reduce((pre, cur) => {
|
||||
return pre + cur.length;
|
||||
}, 0)
|
||||
: 0;
|
||||
}, [value]);
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex space-x-2 bg-bg-card p-1 rounded-md">
|
||||
<Button
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-md hover:text-text-primary hover:bg-bg-base',
|
||||
{
|
||||
'bg-bg-base text-text-primary': active === LogTabs.FILE_LOGS,
|
||||
'bg-transparent text-text-secondary ':
|
||||
active !== LogTabs.FILE_LOGS,
|
||||
},
|
||||
)}
|
||||
onClick={() => setActive?.(LogTabs.FILE_LOGS)}
|
||||
>
|
||||
{t('knowledgeDetails.fileLogs')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-md hover:text-text-primary hover:bg-bg-base',
|
||||
{
|
||||
'bg-bg-base text-text-primary': active === LogTabs.DATASET_LOGS,
|
||||
'bg-transparent text-text-secondary ':
|
||||
active !== LogTabs.DATASET_LOGS,
|
||||
},
|
||||
)}
|
||||
onClick={() => setActive?.(LogTabs.DATASET_LOGS)}
|
||||
>
|
||||
{t('knowledgeDetails.datasetLogs')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<FilterPopover
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
filters={filters}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<FilterButton count={filterCount}></FilterButton>
|
||||
</FilterPopover>
|
||||
|
||||
<SearchInput
|
||||
value={searchString}
|
||||
onChange={onSearchChange}
|
||||
className="w-32"
|
||||
></SearchInput>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { DatasetFilter };
|
||||
98
ragflow_web/src/pages/dataset/dataset-overview/hook.ts
Normal file
98
ragflow_web/src/pages/dataset/dataset-overview/hook.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit';
|
||||
import {
|
||||
useGetPaginationWithRouter,
|
||||
useHandleSearchChange,
|
||||
} from '@/hooks/logic-hooks';
|
||||
import kbService, {
|
||||
listDataPipelineLogDocument,
|
||||
listPipelineDatasetLogs,
|
||||
} from '@/services/knowledge-service';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useParams, useSearchParams } from 'umi';
|
||||
import { LogTabs } from './dataset-common';
|
||||
import { IFileLogList, IOverviewTital } from './interface';
|
||||
|
||||
const useFetchOverviewTital = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { id } = useParams();
|
||||
const knowledgeBaseId = searchParams.get('id') || id;
|
||||
const { data } = useQuery<IOverviewTital>({
|
||||
queryKey: ['overviewTital'],
|
||||
queryFn: async () => {
|
||||
const { data: res = {} } = await kbService.getKnowledgeBasicInfo({
|
||||
kb_id: knowledgeBaseId,
|
||||
});
|
||||
return res.data || [];
|
||||
},
|
||||
});
|
||||
return { data };
|
||||
};
|
||||
|
||||
const useFetchFileLogList = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { searchString, handleInputChange } = useHandleSearchChange();
|
||||
const { pagination, setPagination } = useGetPaginationWithRouter();
|
||||
const { filterValue, setFilterValue, handleFilterSubmit } =
|
||||
useHandleFilterSubmit();
|
||||
const { id } = useParams();
|
||||
const [active, setActive] = useState<(typeof LogTabs)[keyof typeof LogTabs]>(
|
||||
LogTabs.FILE_LOGS,
|
||||
);
|
||||
const knowledgeBaseId = searchParams.get('id') || id;
|
||||
const fetchFunc =
|
||||
active === LogTabs.DATASET_LOGS
|
||||
? listPipelineDatasetLogs
|
||||
: listDataPipelineLogDocument;
|
||||
const { data } = useQuery<IFileLogList>({
|
||||
queryKey: [
|
||||
'fileLogList',
|
||||
knowledgeBaseId,
|
||||
pagination,
|
||||
searchString,
|
||||
active,
|
||||
filterValue,
|
||||
],
|
||||
placeholderData: (previousData) => {
|
||||
if (previousData === undefined) {
|
||||
return { logs: [], total: 0 };
|
||||
}
|
||||
return previousData;
|
||||
},
|
||||
enabled: true,
|
||||
queryFn: async () => {
|
||||
const { data: res = {} } = await fetchFunc(
|
||||
{
|
||||
kb_id: knowledgeBaseId,
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keywords: searchString,
|
||||
// order_by: '',
|
||||
},
|
||||
{ ...filterValue },
|
||||
);
|
||||
return res.data || [];
|
||||
},
|
||||
});
|
||||
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(e) => {
|
||||
setPagination({ page: 1 });
|
||||
handleInputChange(e);
|
||||
},
|
||||
[handleInputChange, setPagination],
|
||||
);
|
||||
return {
|
||||
data,
|
||||
searchString,
|
||||
handleInputChange: onInputChange,
|
||||
pagination: { ...pagination, total: data?.total },
|
||||
setPagination,
|
||||
active,
|
||||
setActive,
|
||||
filterValue,
|
||||
setFilterValue,
|
||||
handleFilterSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export { useFetchFileLogList, useFetchOverviewTital };
|
||||
333
ragflow_web/src/pages/dataset/dataset-overview/index.tsx
Normal file
333
ragflow_web/src/pages/dataset/dataset-overview/index.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
import FileStatusBadge from '@/components/file-status-badge';
|
||||
import { FilterCollection } from '@/components/list-filter-bar/interface';
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import { useIsDarkTheme } from '@/components/theme-provider';
|
||||
import { AntToolTip } from '@/components/ui/tooltip';
|
||||
import { RunningStatusMap } from '@/constants/knowledge';
|
||||
import { useFetchDocumentList } from '@/hooks/use-document-request';
|
||||
import { CircleQuestionMark } from 'lucide-react';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RunningStatus } from '../dataset/constant';
|
||||
import { LogTabs } from './dataset-common';
|
||||
import { DatasetFilter } from './dataset-filter';
|
||||
import { useFetchFileLogList, useFetchOverviewTital } from './hook';
|
||||
import { DocumentLog, IFileLogItem } from './interface';
|
||||
import FileLogsTable from './overview-table';
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: number;
|
||||
icon: JSX.Element;
|
||||
children?: JSX.Element;
|
||||
tooltip?: string;
|
||||
}
|
||||
interface CardFooterProcessProps {
|
||||
success: number;
|
||||
failed: number;
|
||||
successTip?: string;
|
||||
failedTip?: string;
|
||||
}
|
||||
|
||||
const StatCard: FC<StatCardProps> = ({
|
||||
title,
|
||||
value,
|
||||
children,
|
||||
icon,
|
||||
tooltip,
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-bg-card p-4 rounded-lg border border-border flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="flex items-center gap-1 text-sm font-medium text-text-secondary">
|
||||
{title}
|
||||
{tooltip && (
|
||||
<AntToolTip title={tooltip} trigger="hover">
|
||||
<CircleQuestionMark size={12} />
|
||||
</AntToolTip>
|
||||
)}
|
||||
</h3>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-text-primary">{value}</div>
|
||||
<div className="h-12 w-full flex items-center">
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CardFooterProcess: FC<CardFooterProcessProps> = ({
|
||||
success = 0,
|
||||
successTip,
|
||||
failed = 0,
|
||||
failedTip,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center flex-col gap-2">
|
||||
<div className="w-full flex justify-between gap-4 rounded-lg text-sm font-bold text-text-primary">
|
||||
<div className="flex items-center justify-between rounded-md w-1/2 p-2 bg-state-success-5">
|
||||
<div className="flex items-center rounded-lg gap-1">
|
||||
<div className="w-2 h-2 rounded-full bg-state-success "></div>
|
||||
<div className="font-normal text-text-secondary text-xs flex items-center gap-1">
|
||||
{t('knowledgeDetails.success')}
|
||||
{successTip && (
|
||||
<AntToolTip title={successTip} trigger="hover">
|
||||
<CircleQuestionMark size={12} />
|
||||
</AntToolTip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>{success || 0}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md w-1/2 bg-state-error-5 p-2">
|
||||
<div className="flex items-center rounded-lg gap-1">
|
||||
<div className="w-2 h-2 rounded-full bg-state-error"></div>
|
||||
<div className="font-normal text-text-secondary text-xs flex items-center gap-1">
|
||||
{t('knowledgeDetails.failed')}
|
||||
{failedTip && (
|
||||
<AntToolTip title={failedTip} trigger="hover">
|
||||
<CircleQuestionMark size={12} />
|
||||
</AntToolTip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>{failed || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FileLogsPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [topAllData, setTopAllData] = useState({
|
||||
totalFiles: {
|
||||
value: 0,
|
||||
precent: 0,
|
||||
},
|
||||
downloads: {
|
||||
value: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
},
|
||||
processing: {
|
||||
value: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
},
|
||||
});
|
||||
const { data: topData } = useFetchOverviewTital();
|
||||
const {
|
||||
pagination: { total: fileTotal },
|
||||
} = useFetchDocumentList();
|
||||
|
||||
useEffect(() => {
|
||||
setTopAllData((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
processing: {
|
||||
value: topData?.processing || 0,
|
||||
success: topData?.finished || 0,
|
||||
failed: topData?.failed || 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [topData]);
|
||||
|
||||
useEffect(() => {
|
||||
setTopAllData((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
totalFiles: {
|
||||
value: fileTotal || 0,
|
||||
precent: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [fileTotal]);
|
||||
|
||||
const {
|
||||
data: tableOriginData,
|
||||
searchString,
|
||||
handleInputChange,
|
||||
pagination,
|
||||
setPagination,
|
||||
active,
|
||||
filterValue,
|
||||
setFilterValue,
|
||||
handleFilterSubmit,
|
||||
setActive,
|
||||
} = useFetchFileLogList();
|
||||
|
||||
const filters = useMemo(() => {
|
||||
const filterCollection: FilterCollection[] = [
|
||||
{
|
||||
field: 'operation_status',
|
||||
label: t('knowledgeDetails.status'),
|
||||
list: Object.values(RunningStatus).map((value) => {
|
||||
// const value = key as RunningStatus;
|
||||
console.log(value);
|
||||
return {
|
||||
id: value,
|
||||
// label: RunningStatusMap[value].label,
|
||||
label: (
|
||||
<FileStatusBadge
|
||||
status={value as RunningStatus}
|
||||
name={RunningStatusMap[value as RunningStatus]}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}),
|
||||
},
|
||||
// {
|
||||
// field: 'types',
|
||||
// label: t('knowledgeDetails.task'),
|
||||
// list: [
|
||||
// {
|
||||
// id: 'Parse',
|
||||
// label: 'Parse',
|
||||
// },
|
||||
// {
|
||||
// id: 'Download',
|
||||
// label: 'Download',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
];
|
||||
if (active === LogTabs.FILE_LOGS) {
|
||||
return filterCollection;
|
||||
}
|
||||
if (active === LogTabs.DATASET_LOGS) {
|
||||
const list = filterCollection.filter((item, index) => index === 0);
|
||||
return list;
|
||||
}
|
||||
return [];
|
||||
}, [active, t]);
|
||||
|
||||
const tableList = useMemo(() => {
|
||||
console.log('tableList', tableOriginData);
|
||||
if (tableOriginData && tableOriginData.logs?.length) {
|
||||
return tableOriginData.logs.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
status: item.operation_status as RunningStatus,
|
||||
statusName: RunningStatusMap[item.operation_status as RunningStatus],
|
||||
} as unknown as IFileLogItem & DocumentLog;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}, [tableOriginData]);
|
||||
|
||||
const changeActiveLogs = (active: (typeof LogTabs)[keyof typeof LogTabs]) => {
|
||||
setFilterValue({});
|
||||
setActive(active);
|
||||
};
|
||||
const handlePaginationChange = ({
|
||||
page,
|
||||
pageSize,
|
||||
}: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}) => {
|
||||
console.log('Pagination changed:', { page, pageSize });
|
||||
setPagination({
|
||||
...pagination,
|
||||
page,
|
||||
pageSize: pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
const isDark = useIsDarkTheme();
|
||||
|
||||
return (
|
||||
<div className="p-5 min-w-[880px] border-border border rounded-lg mr-5">
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-3 md:grid-cols-3 gap-4 mb-6">
|
||||
<StatCard
|
||||
title={t('datasetOverview.totalFiles')}
|
||||
value={topAllData.totalFiles.value}
|
||||
icon={
|
||||
isDark ? (
|
||||
<SvgIcon name="data-flow/total-files-icon" width={40} />
|
||||
) : (
|
||||
<SvgIcon name="data-flow/total-files-icon-bri" width={40} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<span className="text-accent-primary">
|
||||
{topAllData.totalFiles.precent > 0 ? '+' : ''}
|
||||
{topAllData.totalFiles.precent}%{' '}
|
||||
</span>
|
||||
<span className="font-normal text-text-secondary text-xs">
|
||||
from last week
|
||||
</span>
|
||||
</div>
|
||||
</StatCard>
|
||||
<StatCard
|
||||
title={t('datasetOverview.downloading')}
|
||||
value={topAllData.downloads.value}
|
||||
icon={
|
||||
isDark ? (
|
||||
<SvgIcon name="data-flow/data-icon" width={40} />
|
||||
) : (
|
||||
<SvgIcon name="data-flow/data-icon-bri" width={40} />
|
||||
)
|
||||
}
|
||||
tooltip={t('datasetOverview.downloadTip')}
|
||||
>
|
||||
<CardFooterProcess
|
||||
success={topAllData.downloads.success}
|
||||
successTip={t('datasetOverview.downloadSuccessTip')}
|
||||
failed={topAllData.downloads.failed}
|
||||
failedTip={t('datasetOverview.downloadFailedTip')}
|
||||
/>
|
||||
</StatCard>
|
||||
<StatCard
|
||||
title={t('datasetOverview.processing')}
|
||||
value={topAllData.processing.value}
|
||||
icon={
|
||||
isDark ? (
|
||||
<SvgIcon name="data-flow/processing-icon" width={40} />
|
||||
) : (
|
||||
<SvgIcon name="data-flow/processing-icon-bri" width={40} />
|
||||
)
|
||||
}
|
||||
tooltip={t('datasetOverview.processingTip')}
|
||||
>
|
||||
<CardFooterProcess
|
||||
success={topAllData.processing.success}
|
||||
successTip={t('datasetOverview.processingSuccessTip')}
|
||||
failed={topAllData.processing.failed}
|
||||
failedTip={t('datasetOverview.processingFailedTip')}
|
||||
/>
|
||||
</StatCard>
|
||||
</div>
|
||||
|
||||
{/* Tabs & Search */}
|
||||
<DatasetFilter
|
||||
filters={filters as FilterCollection[]}
|
||||
value={filterValue}
|
||||
active={active}
|
||||
setActive={changeActiveLogs}
|
||||
searchString={searchString}
|
||||
onSearchChange={handleInputChange}
|
||||
onChange={handleFilterSubmit}
|
||||
/>
|
||||
|
||||
{/* Table */}
|
||||
<FileLogsTable
|
||||
data={tableList}
|
||||
pagination={pagination}
|
||||
setPagination={handlePaginationChange}
|
||||
pageCount={10}
|
||||
active={active}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileLogsPage;
|
||||
62
ragflow_web/src/pages/dataset/dataset-overview/interface.ts
Normal file
62
ragflow_web/src/pages/dataset/dataset-overview/interface.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { RunningStatus, RunningStatusMap } from '../dataset/constant';
|
||||
import { LogTabs } from './dataset-common';
|
||||
|
||||
export interface DocumentLog {
|
||||
fileName: string;
|
||||
status: RunningStatus;
|
||||
statusName: typeof RunningStatusMap;
|
||||
}
|
||||
|
||||
export interface FileLogsTableProps {
|
||||
data: Array<IFileLogItem & DocumentLog>;
|
||||
pageCount: number;
|
||||
pagination: {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
setPagination: (pagination: { page: number; pageSize: number }) => void;
|
||||
loading?: boolean;
|
||||
active: (typeof LogTabs)[keyof typeof LogTabs];
|
||||
}
|
||||
|
||||
export interface IOverviewTital {
|
||||
cancelled: number;
|
||||
failed: number;
|
||||
finished: number;
|
||||
processing: number;
|
||||
}
|
||||
|
||||
export interface IFileLogItem {
|
||||
create_date: string;
|
||||
create_time: number;
|
||||
document_id: string;
|
||||
document_name: string;
|
||||
document_suffix: string;
|
||||
document_type: string;
|
||||
dsl: any;
|
||||
path: string[];
|
||||
task_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
kb_id: string;
|
||||
operation_status: string;
|
||||
parser_id: string;
|
||||
pipeline_id: string;
|
||||
pipeline_title: string;
|
||||
avatar: string;
|
||||
process_begin_at: null | string;
|
||||
process_duration: number;
|
||||
progress: number;
|
||||
progress_msg: string;
|
||||
source_from: string;
|
||||
status: string;
|
||||
task_type: string;
|
||||
tenant_id: string;
|
||||
update_date: string;
|
||||
update_time: number;
|
||||
}
|
||||
export interface IFileLogList {
|
||||
logs: Array<IFileLogItem & DocumentLog>;
|
||||
total: number;
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
import FileStatusBadge from '@/components/file-status-badge';
|
||||
import { FileIcon, IconFontFill } from '@/components/icon-font';
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { RunningStatusMap } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { PipelineResultSearchParams } from '@/pages/dataflow-result/constant';
|
||||
import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface';
|
||||
import { formatDate, formatSecondsToHumanReadable } from '@/utils/date';
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
Row,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { TFunction } from 'i18next';
|
||||
import { ArrowUpDown, ClipboardList, Eye } from 'lucide-react';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { useParams } from 'umi';
|
||||
import { RunningStatus } from '../dataset/constant';
|
||||
import ProcessLogModal from '../process-log-modal';
|
||||
import { LogTabs, ProcessingType, ProcessingTypeMap } from './dataset-common';
|
||||
import { DocumentLog, FileLogsTableProps, IFileLogItem } from './interface';
|
||||
|
||||
export const getFileLogsTableColumns = (
|
||||
t: TFunction<'translation', string>,
|
||||
showLog: (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => void,
|
||||
kowledgeId: string,
|
||||
navigateToDataflowResult: (
|
||||
props: NavigateToDataflowResultProps,
|
||||
) => () => void,
|
||||
) => {
|
||||
// const { t } = useTranslate('knowledgeDetails');
|
||||
const columns: ColumnDef<IFileLogItem & DocumentLog>[] = [
|
||||
// {
|
||||
// id: 'select',
|
||||
// header: ({ table }) => (
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={table.getIsAllRowsSelected()}
|
||||
// onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
// className="rounded bg-gray-900 text-blue-500 focus:ring-blue-500"
|
||||
// />
|
||||
// ),
|
||||
// cell: ({ row }) => (
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={row.getIsSelected()}
|
||||
// onChange={row.getToggleSelectedHandler()}
|
||||
// className="rounded border-gray-600 bg-gray-900 text-blue-500 focus:ring-blue-500"
|
||||
// />
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
cell: ({ row }) => (
|
||||
<div className="text-text-primary">{row.original.id}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'fileName',
|
||||
header: t('fileName'),
|
||||
cell: ({ row }) => (
|
||||
<div
|
||||
className="flex items-center gap-2 text-text-primary"
|
||||
// onClick={navigateToDataflowResult(
|
||||
// row.original.id,
|
||||
// row.original.kb_id,
|
||||
// )}
|
||||
>
|
||||
<FileIcon name={row.original.document_name}></FileIcon>
|
||||
{row.original.document_name}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'source_from',
|
||||
header: t('source'),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-text-primary">
|
||||
{row.original.source_from || t('localUpload')}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'pipeline_title',
|
||||
header: t('dataPipeline'),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2 text-text-primary">
|
||||
<RAGFlowAvatar
|
||||
avatar={row.original.avatar}
|
||||
name={row.original.pipeline_title}
|
||||
className="size-4"
|
||||
/>
|
||||
{row.original.pipeline_title === 'naive'
|
||||
? 'general'
|
||||
: row.original.pipeline_title}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'process_begin_at',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="border-none"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
{t('startDate')}
|
||||
<ArrowUpDown />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className="text-text-primary">
|
||||
{formatDate(row.original.process_begin_at)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'task_type',
|
||||
header: t('task'),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-text-primary">{row.original.task_type}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'operation_status',
|
||||
header: t('status'),
|
||||
cell: ({ row }) => (
|
||||
<FileStatusBadge
|
||||
status={row.original.operation_status as RunningStatus}
|
||||
name={
|
||||
RunningStatusMap[row.original.operation_status as RunningStatus]
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
header: t('operations'),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-start space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="p-1"
|
||||
onClick={() => {
|
||||
showLog(row, LogTabs.FILE_LOGS);
|
||||
}}
|
||||
>
|
||||
<Eye />
|
||||
</Button>
|
||||
{row.original.pipeline_id && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="p-1"
|
||||
onClick={navigateToDataflowResult({
|
||||
id: row.original.id,
|
||||
[PipelineResultSearchParams.KnowledgeId]: kowledgeId,
|
||||
[PipelineResultSearchParams.DocumentId]:
|
||||
row.original.document_id,
|
||||
[PipelineResultSearchParams.IsReadOnly]: 'false',
|
||||
[PipelineResultSearchParams.Type]: 'dataflow',
|
||||
})}
|
||||
>
|
||||
<ClipboardList />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getDatasetLogsTableColumns = (
|
||||
t: TFunction<'translation', string>,
|
||||
showLog: (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => void,
|
||||
) => {
|
||||
// const { t } = useTranslate('knowledgeDetails');
|
||||
const columns: ColumnDef<IFileLogItem & DocumentLog>[] = [
|
||||
// {
|
||||
// id: 'select',
|
||||
// header: ({ table }) => (
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={table.getIsAllRowsSelected()}
|
||||
// onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
// className="rounded bg-gray-900 text-blue-500 focus:ring-blue-500"
|
||||
// />
|
||||
// ),
|
||||
// cell: ({ row }) => (
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={row.getIsSelected()}
|
||||
// onChange={row.getToggleSelectedHandler()}
|
||||
// className="rounded border-gray-600 bg-gray-900 text-blue-500 focus:ring-blue-500"
|
||||
// />
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
cell: ({ row }) => (
|
||||
<div className="text-text-primary">{row.original.id}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'process_begin_at',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="border-none"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
{t('startDate')}
|
||||
<ArrowUpDown />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className="text-text-primary">
|
||||
{formatDate(row.original.process_begin_at)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'task_type',
|
||||
header: t('processingType'),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2 text-text-primary">
|
||||
{ProcessingType.knowledgeGraph === row.original.task_type && (
|
||||
<IconFontFill
|
||||
name={`knowledgegraph`}
|
||||
className="text-text-secondary"
|
||||
></IconFontFill>
|
||||
)}
|
||||
{ProcessingType.raptor === row.original.task_type && (
|
||||
<IconFontFill
|
||||
name={`dataflow-01`}
|
||||
className="text-text-secondary"
|
||||
></IconFontFill>
|
||||
)}
|
||||
{ProcessingTypeMap[row.original.task_type as ProcessingType] ||
|
||||
row.original.task_type}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'operation_status',
|
||||
header: t('status'),
|
||||
cell: ({ row }) => (
|
||||
// <FileStatusBadge
|
||||
// status={row.original.status}
|
||||
// name={row.original.statusName}
|
||||
// />
|
||||
<FileStatusBadge
|
||||
status={row.original.operation_status as RunningStatus}
|
||||
name={
|
||||
RunningStatusMap[row.original.operation_status as RunningStatus]
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
header: t('operations'),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-start space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="p-1"
|
||||
onClick={() => {
|
||||
showLog(row, LogTabs.DATASET_LOGS);
|
||||
}}
|
||||
>
|
||||
<Eye />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
const FileLogsTable: FC<FileLogsTableProps> = ({
|
||||
data,
|
||||
pagination,
|
||||
setPagination,
|
||||
active = LogTabs.FILE_LOGS,
|
||||
}) => {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const { navigateToDataflowResult } = useNavigatePage();
|
||||
const [logInfo, setLogInfo] = useState<IFileLogItem>();
|
||||
const kowledgeId = useParams().id;
|
||||
const showLog = (row: Row<IFileLogItem & DocumentLog>) => {
|
||||
const logDetail = {
|
||||
taskId: row.original?.dsl?.task_id,
|
||||
fileName: row.original.document_name,
|
||||
source: row.original.source_from,
|
||||
task: row.original?.task_type,
|
||||
status: row.original.status as RunningStatus,
|
||||
startDate: formatDate(row.original.process_begin_at),
|
||||
duration: formatSecondsToHumanReadable(
|
||||
row.original.process_duration || 0,
|
||||
),
|
||||
details: row.original.progress_msg,
|
||||
} as unknown as IFileLogItem;
|
||||
console.log('logDetail', logDetail);
|
||||
setLogInfo(logDetail);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return active === LogTabs.FILE_LOGS
|
||||
? getFileLogsTableColumns(
|
||||
t,
|
||||
showLog,
|
||||
kowledgeId || '',
|
||||
navigateToDataflowResult,
|
||||
)
|
||||
: getDatasetLogsTableColumns(t, showLog);
|
||||
}, [active, t]);
|
||||
|
||||
const currentPagination = useMemo(
|
||||
() => ({
|
||||
pageIndex: (pagination.current || 1) - 1,
|
||||
pageSize: pagination.pageSize || 10,
|
||||
}),
|
||||
[pagination],
|
||||
);
|
||||
|
||||
const table = useReactTable<IFileLogItem & DocumentLog>({
|
||||
data: data || [],
|
||||
columns,
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
rowSelection,
|
||||
pagination: currentPagination,
|
||||
},
|
||||
pageCount: pagination.total
|
||||
? Math.ceil(pagination.total / pagination.pageSize)
|
||||
: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full h-[calc(100vh-360px)]">
|
||||
<Table rootClassName="max-h-[calc(100vh-380px)]">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="relative min-w-[1280px] overflow-auto">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className="group"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={cell.column.columnDef.meta?.cellClassName}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex items-center justify-end absolute bottom-3 right-12">
|
||||
<div className="space-x-2">
|
||||
<RAGFlowPagination
|
||||
{...{ current: pagination.current, pageSize: pagination.pageSize }}
|
||||
total={pagination.total}
|
||||
onChange={(page, pageSize) => setPagination({ page, pageSize })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isModalVisible && (
|
||||
<ProcessLogModal
|
||||
title={active === LogTabs.FILE_LOGS ? t('fileLogs') : t('datasetLog')}
|
||||
visible={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
logInfo={logInfo}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileLogsTable;
|
||||
@@ -0,0 +1,72 @@
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useSelectParserList } from '@/hooks/user-setting-hooks';
|
||||
import { Col, Divider, Empty, Row, Typography } from 'antd';
|
||||
import DOMPurify from 'dompurify';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import { useMemo } from 'react';
|
||||
import { TagTabs } from './tag-tabs';
|
||||
import { ImageMap } from './utils';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => {
|
||||
const parserList = useSelectParserList();
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
|
||||
const item = useMemo(() => {
|
||||
const item = parserList.find((x) => x.value === chunkMethod);
|
||||
if (item) {
|
||||
return {
|
||||
title: item.label,
|
||||
description: t(camelCase(item.value)),
|
||||
};
|
||||
}
|
||||
return { title: '', description: '' };
|
||||
}, [parserList, chunkMethod, t]);
|
||||
|
||||
const imageList = useMemo(() => {
|
||||
if (chunkMethod in ImageMap) {
|
||||
return ImageMap[chunkMethod as keyof typeof ImageMap];
|
||||
}
|
||||
return [];
|
||||
}, [chunkMethod]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
{imageList.length > 0 ? (
|
||||
<>
|
||||
<h5 className="font-semibold text-base mt-0 mb-1">
|
||||
{`"${item.title}" ${t('methodTitle')}`}
|
||||
</h5>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(item.description),
|
||||
}}
|
||||
></p>
|
||||
<h5 className="font-semibold text-base mt-4 mb-1">{`"${item.title}" ${t('methodExamples')}`}</h5>
|
||||
<Text>{t('methodExamplesDescription')}</Text>
|
||||
<Row gutter={[10, 10]} className="mt-4">
|
||||
{imageList.map((x) => (
|
||||
<Col span={12} key={x}>
|
||||
<SvgIcon name={x} width={'100%'} className="w-full"></SvgIcon>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<h5 className="font-semibold text-base mt-4 mb-1">
|
||||
{item.title} {t('dialogueExamplesTitle')}
|
||||
</h5>
|
||||
<Divider></Divider>
|
||||
</>
|
||||
) : (
|
||||
<Empty description={''} image={null}>
|
||||
<p>{t('methodEmpty')}</p>
|
||||
<SvgIcon name={'chunk-method/chunk-empty'} width={'100%'}></SvgIcon>
|
||||
</Empty>
|
||||
)}
|
||||
{chunkMethod === 'tag' && <TagTabs></TagTabs>}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryPanel;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useMemo } from 'react';
|
||||
import { AudioConfiguration } from './configuration/audio';
|
||||
import { BookConfiguration } from './configuration/book';
|
||||
import { EmailConfiguration } from './configuration/email';
|
||||
import { KnowledgeGraphConfiguration } from './configuration/knowledge-graph';
|
||||
import { LawsConfiguration } from './configuration/laws';
|
||||
import { ManualConfiguration } from './configuration/manual';
|
||||
import { NaiveConfiguration } from './configuration/naive';
|
||||
import { OneConfiguration } from './configuration/one';
|
||||
import { PaperConfiguration } from './configuration/paper';
|
||||
import { PictureConfiguration } from './configuration/picture';
|
||||
import { PresentationConfiguration } from './configuration/presentation';
|
||||
import { QAConfiguration } from './configuration/qa';
|
||||
import { ResumeConfiguration } from './configuration/resume';
|
||||
import { TableConfiguration } from './configuration/table';
|
||||
import { TagConfiguration } from './configuration/tag';
|
||||
|
||||
const ConfigurationComponentMap = {
|
||||
[DocumentParserType.Naive]: NaiveConfiguration,
|
||||
[DocumentParserType.Qa]: QAConfiguration,
|
||||
[DocumentParserType.Resume]: ResumeConfiguration,
|
||||
[DocumentParserType.Manual]: ManualConfiguration,
|
||||
[DocumentParserType.Table]: TableConfiguration,
|
||||
[DocumentParserType.Paper]: PaperConfiguration,
|
||||
[DocumentParserType.Book]: BookConfiguration,
|
||||
[DocumentParserType.Laws]: LawsConfiguration,
|
||||
[DocumentParserType.Presentation]: PresentationConfiguration,
|
||||
[DocumentParserType.Picture]: PictureConfiguration,
|
||||
[DocumentParserType.One]: OneConfiguration,
|
||||
[DocumentParserType.Audio]: AudioConfiguration,
|
||||
[DocumentParserType.Email]: EmailConfiguration,
|
||||
[DocumentParserType.Tag]: TagConfiguration,
|
||||
[DocumentParserType.KnowledgeGraph]: KnowledgeGraphConfiguration,
|
||||
};
|
||||
|
||||
function EmptyComponent() {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export function ChunkMethodForm() {
|
||||
const form = useFormContext();
|
||||
|
||||
const finalParserId: DocumentParserType = useWatch({
|
||||
control: form.control,
|
||||
name: 'parser_id',
|
||||
});
|
||||
|
||||
const ConfigurationComponent = useMemo(() => {
|
||||
return finalParserId
|
||||
? ConfigurationComponentMap[finalParserId]
|
||||
: EmptyComponent;
|
||||
}, [finalParserId]);
|
||||
|
||||
return (
|
||||
<section className="h-full flex flex-col">
|
||||
<div className="overflow-auto flex-1 min-h-0">
|
||||
<ConfigurationComponent></ConfigurationComponent>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
import { X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CategoryPanel from './category-panel';
|
||||
|
||||
export default ({ parserId }: { parserId: string }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={cn('hidden flex-1', 'flex flex-col')}>
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setVisible(!visible);
|
||||
}}
|
||||
>
|
||||
{t('knowledgeDetails.learnMore')}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="bg-[#FFF]/10 p-[20px] rounded-[12px] mt-[10px] relative flex-1 overflow-auto"
|
||||
style={{ display: visible ? 'block' : 'none' }}
|
||||
>
|
||||
<CategoryPanel chunkMethod={parserId}></CategoryPanel>
|
||||
<div
|
||||
className="absolute right-1 top-1 cursor-pointer hover:text-[#FFF]/30"
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<X />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import { IDataPipelineSelectNode } from '@/components/data-pipeline-select';
|
||||
import { IconFont } from '@/components/icon-font';
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { Link } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LinkDataPipelineModal from './link-data-pipline-modal';
|
||||
export interface IDataPipelineNodeProps extends IDataPipelineSelectNode {
|
||||
isDefault?: boolean;
|
||||
linked?: boolean;
|
||||
}
|
||||
|
||||
export interface ILinkDataPipelineProps {
|
||||
data?: IDataPipelineNodeProps;
|
||||
handleLinkOrEditSubmit?: (data: IDataPipelineNodeProps | undefined) => void;
|
||||
}
|
||||
|
||||
interface DataPipelineItemProps extends IDataPipelineNodeProps {
|
||||
openLinkModalFunc?: (open: boolean, data?: IDataPipelineNodeProps) => void;
|
||||
}
|
||||
|
||||
const DataPipelineItem = (props: DataPipelineItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { name, avatar, isDefault, linked, openLinkModalFunc } = props;
|
||||
const openUnlinkModal = () => {
|
||||
Modal.show({
|
||||
visible: true,
|
||||
className: '!w-[560px]',
|
||||
title: t('dataflowParser.unlinkPipelineModalTitle'),
|
||||
children: (
|
||||
<div
|
||||
className="text-sm text-text-secondary"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('dataflowParser.unlinkPipelineModalContent'),
|
||||
}}
|
||||
></div>
|
||||
),
|
||||
onVisibleChange: () => {
|
||||
Modal.hide();
|
||||
},
|
||||
footer: (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant={'outline'} onClick={() => Modal.hide()}>
|
||||
{t('dataflowParser.changeStepModalCancelText')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
className="!bg-state-error text-bg-base"
|
||||
onClick={() => {
|
||||
Modal.hide();
|
||||
}}
|
||||
>
|
||||
{t('dataflowParser.unlinkPipelineModalConfirmText')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-1 px-2 rounded-md border">
|
||||
<div className="flex items-center gap-1">
|
||||
<RAGFlowAvatar avatar={avatar} name={name} className="size-4" />
|
||||
<div>{name}</div>
|
||||
{/* {isDefault && (
|
||||
<div className="text-xs bg-text-secondary text-bg-base px-2 py-1 rounded-md">
|
||||
{t('knowledgeConfiguration.default')}
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
{/* <div className="flex gap-1 items-center">
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
className="border-none"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openLinkModalFunc?.(true, { ...omit(props, ['openLinkModalFunc']) })
|
||||
}
|
||||
>
|
||||
<Settings2 />
|
||||
</Button>
|
||||
{!isDefault && (
|
||||
<>
|
||||
{linked && (
|
||||
<Button
|
||||
type="button"
|
||||
variant={'transparent'}
|
||||
className="border-none"
|
||||
onClick={() => {
|
||||
openUnlinkModal();
|
||||
}}
|
||||
>
|
||||
<Unlink />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkDataPipeline = (props: ILinkDataPipelineProps) => {
|
||||
const { data, handleLinkOrEditSubmit: submit } = props;
|
||||
const { t } = useTranslation();
|
||||
const [openLinkModal, setOpenLinkModal] = useState(false);
|
||||
const [currentDataPipeline, setCurrentDataPipeline] =
|
||||
useState<IDataPipelineNodeProps>();
|
||||
const pipelineNode: IDataPipelineNodeProps[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: data?.id,
|
||||
name: data?.name,
|
||||
avatar: data?.avatar,
|
||||
isDefault: data?.isDefault,
|
||||
linked: true,
|
||||
},
|
||||
],
|
||||
[data],
|
||||
);
|
||||
const openLinkModalFunc = (open: boolean, data?: IDataPipelineNodeProps) => {
|
||||
console.log('open', open, data);
|
||||
setOpenLinkModal(open);
|
||||
if (data) {
|
||||
setCurrentDataPipeline(data);
|
||||
} else {
|
||||
setCurrentDataPipeline(undefined);
|
||||
}
|
||||
};
|
||||
const handleLinkOrEditSubmit = (
|
||||
data: IDataPipelineSelectNode | undefined,
|
||||
) => {
|
||||
console.log('handleLinkOrEditSubmit', data);
|
||||
submit?.(data);
|
||||
setOpenLinkModal(false);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<section className="flex flex-col">
|
||||
<div className="flex items-center gap-1 text-text-primary text-sm">
|
||||
<IconFont name="Pipeline" />
|
||||
{t('knowledgeConfiguration.dataPipeline')}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-center text-xs text-text-secondary">
|
||||
{t('knowledgeConfiguration.linkPipelineSetTip')}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant={'transparent'}
|
||||
onClick={() => {
|
||||
openLinkModalFunc?.(true);
|
||||
}}
|
||||
>
|
||||
<Link />
|
||||
<span className="text-xs text-text-primary">
|
||||
{t('knowledgeConfiguration.linkDataPipeline')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
<section className="flex flex-col gap-2">
|
||||
{pipelineNode.map(
|
||||
(item) =>
|
||||
item.id && (
|
||||
<DataPipelineItem
|
||||
key={item.id}
|
||||
openLinkModalFunc={openLinkModalFunc}
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
avatar={item.avatar}
|
||||
isDefault={item.isDefault}
|
||||
linked={item.linked}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</section>
|
||||
<LinkDataPipelineModal
|
||||
data={currentDataPipeline}
|
||||
open={openLinkModal}
|
||||
setOpen={(open: boolean) => {
|
||||
openLinkModalFunc(open);
|
||||
}}
|
||||
onSubmit={handleLinkOrEditSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default LinkDataPipeline;
|
||||
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
DataFlowSelect,
|
||||
IDataPipelineSelectNode,
|
||||
} from '@/components/data-pipeline-select';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { t } from 'i18next';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { pipelineFormSchema } from '../form-schema';
|
||||
import { IDataPipelineNodeProps } from './link-data-pipeline';
|
||||
|
||||
const LinkDataPipelineModal = ({
|
||||
data,
|
||||
open,
|
||||
setOpen,
|
||||
onSubmit,
|
||||
}: {
|
||||
data: IDataPipelineNodeProps | undefined;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
onSubmit?: (pipeline: IDataPipelineSelectNode | undefined) => void;
|
||||
}) => {
|
||||
const isEdit = !!data;
|
||||
const [list, setList] = useState<IDataPipelineSelectNode[]>();
|
||||
const form = useForm<z.infer<typeof pipelineFormSchema>>({
|
||||
resolver: zodResolver(pipelineFormSchema),
|
||||
defaultValues: {
|
||||
pipeline_id: '',
|
||||
set_default: false,
|
||||
file_filter: '',
|
||||
},
|
||||
});
|
||||
// const [open, setOpen] = useState(false);
|
||||
const { navigateToAgents } = useNavigatePage();
|
||||
const handleFormSubmit = (values: any) => {
|
||||
console.log(values, data);
|
||||
// const param = {
|
||||
// ...data,
|
||||
// ...values,
|
||||
// };
|
||||
const pipeline = list?.find((item) => item.id === values.pipeline_id);
|
||||
onSubmit?.(pipeline);
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
className="!w-[560px]"
|
||||
title={
|
||||
!isEdit
|
||||
? t('knowledgeConfiguration.linkDataPipeline')
|
||||
: t('knowledgeConfiguration.eidtLinkDataPipeline')
|
||||
}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
showfooter={false}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex flex-col gap-4 ">
|
||||
{!isEdit && (
|
||||
<DataFlowSelect
|
||||
toDataPipeline={navigateToAgents}
|
||||
formFieldName="pipeline_id"
|
||||
setDataList={setList}
|
||||
/>
|
||||
)}
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name={'file_filter'}
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-2 justify-between ">
|
||||
<FormLabel
|
||||
tooltip={t('knowledgeConfiguration.fileFilterTip')}
|
||||
className="text-sm text-text-primary whitespace-wrap "
|
||||
>
|
||||
{t('knowledgeConfiguration.fileFilter')}
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t(
|
||||
'knowledgeConfiguration.filterPlaceholder',
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-full"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isEdit && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'set_default'}
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-2 justify-between ">
|
||||
<FormLabel
|
||||
tooltip={t('knowledgeConfiguration.setDefaultTip')}
|
||||
className="text-sm text-text-primary whitespace-wrap "
|
||||
>
|
||||
{t('knowledgeConfiguration.setDefault')}
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground">
|
||||
<FormControl>
|
||||
<Switch
|
||||
value={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-full"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)} */}
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'outline'}
|
||||
className="btn-primary"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('modal.cancelText')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={'default'}
|
||||
className="btn-primary"
|
||||
onClick={form.handleSubmit(handleFormSubmit)}
|
||||
>
|
||||
{t('modal.okText')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default LinkDataPipelineModal;
|
||||
@@ -0,0 +1,138 @@
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { MultiSelect } from '@/components/ui/multi-select';
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
||||
import { Form, Select, Space } from 'antd';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const TagSetItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
|
||||
const { list: knowledgeList } = useFetchKnowledgeList(true);
|
||||
|
||||
const knowledgeOptions = knowledgeList
|
||||
.filter((x) => x.parser_id === 'tag')
|
||||
.map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
icon: () => (
|
||||
<Space>
|
||||
<RAGFlowAvatar
|
||||
name={x.name}
|
||||
avatar={x.avatar}
|
||||
className="size-4"
|
||||
></RAGFlowAvatar>
|
||||
</Space>
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="parser_config.tag_kb_ids"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
className="text-sm text-text-secondary whitespace-nowrap w-1/4"
|
||||
tooltip={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(
|
||||
t('knowledgeConfiguration.tagSetTip'),
|
||||
),
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
>
|
||||
{t('knowledgeConfiguration.tagSet')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={knowledgeOptions}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('chat.knowledgeBasesMessage')}
|
||||
variant="inverted"
|
||||
maxCount={10}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label={t('knowledgeConfiguration.tagSet')}
|
||||
name={['parser_config', 'tag_kb_ids']}
|
||||
tooltip={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(t('knowledgeConfiguration.tagSetTip')),
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
message: t('chat.knowledgeBasesMessage'),
|
||||
type: 'array',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
options={knowledgeOptions}
|
||||
placeholder={t('chat.knowledgeBasesMessage')}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const TopNTagsItem = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SliderInputFormField
|
||||
name={'parser_config.topn_tags'}
|
||||
label={t('knowledgeConfiguration.topnTags')}
|
||||
max={10}
|
||||
min={1}
|
||||
defaultValue={3}
|
||||
layout={FormLayout.Horizontal}
|
||||
></SliderInputFormField>
|
||||
);
|
||||
};
|
||||
|
||||
export function TagItems() {
|
||||
const form = useFormContext();
|
||||
const ids: string[] = useWatch({
|
||||
control: form.control,
|
||||
name: 'parser_config.tag_kb_ids',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<TagSetItem></TagSetItem>
|
||||
{Array.isArray(ids) && ids.length > 0 && <TopNTagsItem></TopNTagsItem>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { FormContainerProps } from '@/components/form-container';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
export function ConfigurationFormContainer({
|
||||
children,
|
||||
className,
|
||||
}: FormContainerProps) {
|
||||
return <section className={cn('space-y-4', className)}>{children}</section>;
|
||||
}
|
||||
|
||||
export function MainContainer({
|
||||
children,
|
||||
className,
|
||||
}: PropsWithChildren & { className?: string }) {
|
||||
return <section className={cn('space-y-5', className)}>{children}</section>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||
|
||||
export function AudioConfiguration() {
|
||||
return (
|
||||
<ConfigurationFormContainer>
|
||||
<>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</>
|
||||
|
||||
{/* <TagItems></TagItems> */}
|
||||
</ConfigurationFormContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||
import {
|
||||
ConfigurationFormContainer,
|
||||
MainContainer,
|
||||
} from '../configuration-form-container';
|
||||
|
||||
export function BookConfiguration() {
|
||||
return (
|
||||
<MainContainer>
|
||||
<ConfigurationFormContainer>
|
||||
<LayoutRecognizeFormField></LayoutRecognizeFormField>
|
||||
</ConfigurationFormContainer>
|
||||
|
||||
<ConfigurationFormContainer>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</ConfigurationFormContainer>
|
||||
{/* <ConfigurationFormContainer>
|
||||
<TagItems></TagItems>
|
||||
</ConfigurationFormContainer> */}
|
||||
</MainContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Radio } from '@/components/ui/radio';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
useHasParsedDocument,
|
||||
useSelectChunkMethodList,
|
||||
useSelectEmbeddingModelOptions,
|
||||
} from '../hooks';
|
||||
interface IProps {
|
||||
line?: 1 | 2;
|
||||
isEdit?: boolean;
|
||||
}
|
||||
export function ChunkMethodItem(props: IProps) {
|
||||
const { line } = props;
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
// const handleChunkMethodSelectChange = useHandleChunkMethodSelectChange(form);
|
||||
const parserList = useSelectChunkMethodList();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'parser_id'}
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-1">
|
||||
<div className={line === 1 ? 'flex items-center' : ''}>
|
||||
<FormLabel
|
||||
required
|
||||
tooltip={t('chunkMethodTip')}
|
||||
className={cn('text-sm', {
|
||||
'w-1/4 whitespace-pre-wrap': line === 1,
|
||||
})}
|
||||
>
|
||||
{t('builtIn')}
|
||||
</FormLabel>
|
||||
<div className={line === 1 ? 'w-3/4 ' : 'w-full'}>
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
options={parserList}
|
||||
placeholder={t('chunkMethodPlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className={line === 1 ? 'w-1/4' : ''}></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export function EmbeddingModelItem({ line = 1, isEdit = true }: IProps) {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
const embeddingModelOptions = useSelectEmbeddingModelOptions();
|
||||
const disabled = useHasParsedDocument(isEdit);
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'embd_id'}
|
||||
render={({ field }) => (
|
||||
<FormItem className={cn(' items-center space-y-0 ')}>
|
||||
<div
|
||||
className={cn('flex', {
|
||||
' items-center': line === 1,
|
||||
'flex-col gap-1': line === 2,
|
||||
})}
|
||||
>
|
||||
<FormLabel
|
||||
required
|
||||
tooltip={t('embeddingModelTip')}
|
||||
className={cn('text-sm whitespace-wrap ', {
|
||||
'w-1/4': line === 1,
|
||||
})}
|
||||
>
|
||||
{t('embeddingModel')}
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
onChange={field.onChange}
|
||||
value={field.value}
|
||||
options={embeddingModelOptions}
|
||||
disabled={isEdit ? disabled : false}
|
||||
placeholder={t('embeddingModelPlaceholder')}
|
||||
triggerClassName="!bg-bg-base"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className={line === 1 ? 'w-1/4' : ''}></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ParseTypeItem({ line = 2 }: { line?: number }) {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'parseType'}
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div
|
||||
className={cn('flex', {
|
||||
' items-center': line === 1,
|
||||
'flex-col gap-1': line === 2,
|
||||
})}
|
||||
>
|
||||
<FormLabel
|
||||
// tooltip={t('parseTypeTip')}
|
||||
className={cn('text-sm whitespace-wrap ', {
|
||||
'w-1/4': line === 1,
|
||||
})}
|
||||
>
|
||||
{t('parseType')}
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
|
||||
>
|
||||
<FormControl>
|
||||
<Radio.Group {...field}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex gap-2 justify-between text-muted-foreground',
|
||||
line === 1 ? 'w-1/2' : 'w-3/4',
|
||||
)}
|
||||
>
|
||||
<Radio value={1}>{t('builtIn')}</Radio>
|
||||
<Radio value={2}>{t('manualSetup')}</Radio>
|
||||
</div>
|
||||
</Radio.Group>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className={line === 1 ? 'w-1/4' : ''}></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function EnableAutoGenerateItem() {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'enableAutoGenerate'}
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={t('enableAutoGenerateTip')}
|
||||
className="text-sm whitespace-wrap w-1/4"
|
||||
>
|
||||
{t('enableAutoGenerate')}
|
||||
</FormLabel>
|
||||
<div className="text-muted-foreground w-3/4">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function EnableTocToggle() {
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'parser_config.toc_extraction'}
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={t('tocExtractionTip')}
|
||||
className="text-sm whitespace-wrap w-1/4"
|
||||
>
|
||||
{t('tocExtraction')}
|
||||
</FormLabel>
|
||||
<div className="text-muted-foreground w-3/4">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||
|
||||
export function EmailConfiguration() {
|
||||
return (
|
||||
<ConfigurationFormContainer>
|
||||
<>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</>
|
||||
{/* <TagItems></TagItems> */}
|
||||
</ConfigurationFormContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { DelimiterFormField } from '@/components/delimiter-form-field';
|
||||
import { EntityTypesFormField } from '@/components/entity-types-form-field';
|
||||
import { MaxTokenNumberFormField } from '@/components/max-token-number-from-field';
|
||||
|
||||
export function KnowledgeGraphConfiguration() {
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<EntityTypesFormField></EntityTypesFormField>
|
||||
<MaxTokenNumberFormField max={8192 * 2}></MaxTokenNumberFormField>
|
||||
<DelimiterFormField></DelimiterFormField>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||
import {
|
||||
ConfigurationFormContainer,
|
||||
MainContainer,
|
||||
} from '../configuration-form-container';
|
||||
|
||||
export function LawsConfiguration() {
|
||||
return (
|
||||
<MainContainer>
|
||||
<ConfigurationFormContainer>
|
||||
<LayoutRecognizeFormField></LayoutRecognizeFormField>
|
||||
</ConfigurationFormContainer>
|
||||
|
||||
<ConfigurationFormContainer>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</ConfigurationFormContainer>
|
||||
|
||||
{/* <ConfigurationFormContainer>
|
||||
<TagItems></TagItems>
|
||||
</ConfigurationFormContainer> */}
|
||||
</MainContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||
import {
|
||||
ConfigurationFormContainer,
|
||||
MainContainer,
|
||||
} from '../configuration-form-container';
|
||||
|
||||
export function ManualConfiguration() {
|
||||
return (
|
||||
<MainContainer>
|
||||
<ConfigurationFormContainer>
|
||||
<LayoutRecognizeFormField></LayoutRecognizeFormField>
|
||||
</ConfigurationFormContainer>
|
||||
|
||||
<ConfigurationFormContainer>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</ConfigurationFormContainer>
|
||||
|
||||
{/* <TagItems></TagItems> */}
|
||||
</MainContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { DelimiterFormField } from '@/components/delimiter-form-field';
|
||||
import { ExcelToHtmlFormField } from '@/components/excel-to-html-form-field';
|
||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||
import { MaxTokenNumberFormField } from '@/components/max-token-number-from-field';
|
||||
import {
|
||||
ConfigurationFormContainer,
|
||||
MainContainer,
|
||||
} from '../configuration-form-container';
|
||||
import { EnableTocToggle } from './common-item';
|
||||
|
||||
export function NaiveConfiguration() {
|
||||
return (
|
||||
<MainContainer>
|
||||
<ConfigurationFormContainer>
|
||||
<LayoutRecognizeFormField></LayoutRecognizeFormField>
|
||||
<MaxTokenNumberFormField initialValue={512}></MaxTokenNumberFormField>
|
||||
<DelimiterFormField></DelimiterFormField>
|
||||
<EnableTocToggle />
|
||||
</ConfigurationFormContainer>
|
||||
<ConfigurationFormContainer>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
<ExcelToHtmlFormField></ExcelToHtmlFormField>
|
||||
{/* <TagItems></TagItems> */}
|
||||
</ConfigurationFormContainer>
|
||||
</MainContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||
|
||||
export function OneConfiguration() {
|
||||
return (
|
||||
<ConfigurationFormContainer>
|
||||
<LayoutRecognizeFormField></LayoutRecognizeFormField>
|
||||
<>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</>
|
||||
|
||||
{/* <TagItems></TagItems> */}
|
||||
</ConfigurationFormContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||
import {
|
||||
ConfigurationFormContainer,
|
||||
MainContainer,
|
||||
} from '../configuration-form-container';
|
||||
|
||||
export function PaperConfiguration() {
|
||||
return (
|
||||
<MainContainer>
|
||||
<ConfigurationFormContainer>
|
||||
<LayoutRecognizeFormField></LayoutRecognizeFormField>
|
||||
</ConfigurationFormContainer>
|
||||
|
||||
<ConfigurationFormContainer>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</ConfigurationFormContainer>
|
||||
{/* <ConfigurationFormContainer>
|
||||
<TagItems></TagItems>
|
||||
</ConfigurationFormContainer> */}
|
||||
</MainContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||
|
||||
export function PictureConfiguration() {
|
||||
return (
|
||||
<ConfigurationFormContainer>
|
||||
<>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</>
|
||||
{/* <TagItems></TagItems> */}
|
||||
</ConfigurationFormContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
AutoKeywordsFormField,
|
||||
AutoQuestionsFormField,
|
||||
} from '@/components/auto-keywords-form-field';
|
||||
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||
import {
|
||||
ConfigurationFormContainer,
|
||||
MainContainer,
|
||||
} from '../configuration-form-container';
|
||||
|
||||
export function PresentationConfiguration() {
|
||||
return (
|
||||
<MainContainer>
|
||||
<ConfigurationFormContainer>
|
||||
<LayoutRecognizeFormField></LayoutRecognizeFormField>
|
||||
</ConfigurationFormContainer>
|
||||
|
||||
<ConfigurationFormContainer>
|
||||
<AutoKeywordsFormField></AutoKeywordsFormField>
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</ConfigurationFormContainer>
|
||||
|
||||
{/* <ConfigurationFormContainer>
|
||||
<TagItems></TagItems>
|
||||
</ConfigurationFormContainer> */}
|
||||
</MainContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function QAConfiguration() {
|
||||
return (
|
||||
<></>
|
||||
// <ConfigurationFormContainer>
|
||||
// <TagItems></TagItems>
|
||||
// </ConfigurationFormContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function ResumeConfiguration() {
|
||||
return (
|
||||
<></>
|
||||
// <ConfigurationFormContainer>
|
||||
// <TagItems></TagItems>
|
||||
// </ConfigurationFormContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||
|
||||
export function TableConfiguration() {
|
||||
return (
|
||||
<ConfigurationFormContainer>
|
||||
{/* <ChunkMethodItem></ChunkMethodItem>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
|
||||
<PageRankFormField></PageRankFormField> */}
|
||||
</ConfigurationFormContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ConfigurationFormContainer } from '../configuration-form-container';
|
||||
|
||||
export function TagConfiguration() {
|
||||
return <ConfigurationFormContainer></ConfigurationFormContainer>;
|
||||
}
|
||||
104
ragflow_web/src/pages/dataset/dataset-setting/form-schema.ts
Normal file
104
ragflow_web/src/pages/dataset/dataset-setting/form-schema.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { t } from 'i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const formSchema = z
|
||||
.object({
|
||||
parseType: z.number(),
|
||||
name: z.string().min(1, {
|
||||
message: 'Username must be at least 2 characters.',
|
||||
}),
|
||||
description: z.string().min(2, {
|
||||
message: 'Username must be at least 2 characters.',
|
||||
}),
|
||||
// avatar: z.instanceof(File),
|
||||
avatar: z.any().nullish(),
|
||||
permission: z.string().optional(),
|
||||
parser_id: z.string(),
|
||||
pipeline_id: z.string().optional(),
|
||||
pipeline_name: z.string().optional(),
|
||||
pipeline_avatar: z.string().optional(),
|
||||
embd_id: z.string(),
|
||||
parser_config: z
|
||||
.object({
|
||||
layout_recognize: z.string(),
|
||||
chunk_token_num: z.number(),
|
||||
delimiter: z.string(),
|
||||
auto_keywords: z.number().optional(),
|
||||
auto_questions: z.number().optional(),
|
||||
html4excel: z.boolean(),
|
||||
tag_kb_ids: z.array(z.string()).nullish(),
|
||||
topn_tags: z.number().optional(),
|
||||
toc_extraction: z.boolean().optional(),
|
||||
raptor: z
|
||||
.object({
|
||||
use_raptor: z.boolean().optional(),
|
||||
prompt: z.string().optional(),
|
||||
max_token: z.number().optional(),
|
||||
threshold: z.number().optional(),
|
||||
max_cluster: z.number().optional(),
|
||||
random_seed: z.number().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.use_raptor && !data.prompt) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'Prompt is required',
|
||||
path: ['prompt'],
|
||||
},
|
||||
),
|
||||
graphrag: z
|
||||
.object({
|
||||
use_graphrag: z.boolean().optional(),
|
||||
entity_types: z.array(z.string()).optional(),
|
||||
method: z.string().optional(),
|
||||
resolution: z.boolean().optional(),
|
||||
community: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (
|
||||
data.use_graphrag &&
|
||||
(!data.entity_types || data.entity_types.length === 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'Please enter Entity types',
|
||||
path: ['entity_types'],
|
||||
},
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
pagerank: z.number(),
|
||||
// icon: z.array(z.instanceof(File)),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.parseType === 2 && !data.pipeline_id) {
|
||||
ctx.addIssue({
|
||||
path: ['pipeline_id'],
|
||||
message: t('common.pleaseSelect'),
|
||||
code: 'custom',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const pipelineFormSchema = z.object({
|
||||
pipeline_id: z.string().optional(),
|
||||
set_default: z.boolean().optional(),
|
||||
file_filter: z.string().optional(),
|
||||
});
|
||||
|
||||
// export const linkPiplineFormSchema = pipelineFormSchema.pick({
|
||||
// pipeline_id: true,
|
||||
// file_filter: true,
|
||||
// });
|
||||
// export const editPiplineFormSchema = pipelineFormSchema.pick({
|
||||
// set_default: true,
|
||||
// file_filter: true,
|
||||
// });
|
||||
@@ -0,0 +1,97 @@
|
||||
import { AvatarUpload } from '@/components/avatar-upload';
|
||||
import PageRankFormField from '@/components/page-rank-form-field';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TagItems } from './components/tag-item';
|
||||
import { EmbeddingModelItem } from './configuration/common-item';
|
||||
import { PermissionFormField } from './permission-form-field';
|
||||
|
||||
export function GeneralForm() {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex">
|
||||
<FormLabel className="text-sm whitespace-nowrap w-1/4">
|
||||
<span className="text-red-600">*</span>
|
||||
{t('common.name')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<Input {...field}></Input>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="avatar"
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex">
|
||||
<FormLabel className="text-sm whitespace-nowrap w-1/4">
|
||||
{t('setting.avatar')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<AvatarUpload {...field}></AvatarUpload>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
// null initialize empty string
|
||||
if (typeof field.value === 'object' && !field.value) {
|
||||
form.setValue('description', ' ');
|
||||
}
|
||||
return (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex">
|
||||
<FormLabel className="text-sm whitespace-nowrap w-1/4">
|
||||
{t('flow.description')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<Input {...field}></Input>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<PermissionFormField></PermissionFormField>
|
||||
<EmbeddingModelItem></EmbeddingModelItem>
|
||||
<PageRankFormField></PageRankFormField>
|
||||
|
||||
<TagItems></TagItems>
|
||||
</>
|
||||
);
|
||||
}
|
||||
99
ragflow_web/src/pages/dataset/dataset-setting/hooks.ts
Normal file
99
ragflow_web/src/pages/dataset/dataset-setting/hooks.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { LlmModelType } from '@/constants/knowledge';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
|
||||
import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks';
|
||||
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
||||
import { useSelectParserList } from '@/hooks/user-setting-hooks';
|
||||
import { useIsFetching } from '@tanstack/react-query';
|
||||
import { pick } from 'lodash';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { formSchema } from './form-schema';
|
||||
|
||||
// The value that does not need to be displayed in the analysis method Select
|
||||
const HiddenFields = ['email', 'picture', 'audio'];
|
||||
|
||||
export function useSelectChunkMethodList() {
|
||||
const parserList = useSelectParserList();
|
||||
|
||||
return parserList.filter((x) => !HiddenFields.some((y) => y === x.value));
|
||||
}
|
||||
|
||||
export function useSelectEmbeddingModelOptions() {
|
||||
const allOptions = useSelectLlmOptionsByModelType();
|
||||
return allOptions[LlmModelType.Embedding];
|
||||
}
|
||||
|
||||
export function useHasParsedDocument(isEdit?: boolean) {
|
||||
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration({
|
||||
isEdit,
|
||||
});
|
||||
return knowledgeDetails.chunk_num > 0;
|
||||
}
|
||||
|
||||
export const useFetchKnowledgeConfigurationOnMount = (
|
||||
form: UseFormReturn<z.infer<typeof formSchema>, any, undefined>,
|
||||
) => {
|
||||
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
|
||||
|
||||
useEffect(() => {
|
||||
const parser_config = {
|
||||
...form.formState?.defaultValues?.parser_config,
|
||||
...knowledgeDetails.parser_config,
|
||||
raptor: {
|
||||
...form.formState?.defaultValues?.parser_config?.raptor,
|
||||
...knowledgeDetails.parser_config?.raptor,
|
||||
use_raptor: true,
|
||||
},
|
||||
graphrag: {
|
||||
...form.formState?.defaultValues?.parser_config?.graphrag,
|
||||
...knowledgeDetails.parser_config?.graphrag,
|
||||
use_graphrag: true,
|
||||
},
|
||||
};
|
||||
const formValues = {
|
||||
...pick({ ...knowledgeDetails, parser_config: parser_config }, [
|
||||
'description',
|
||||
'name',
|
||||
'permission',
|
||||
'embd_id',
|
||||
'parser_id',
|
||||
'language',
|
||||
'parser_config',
|
||||
'pagerank',
|
||||
'avatar',
|
||||
]),
|
||||
} as z.infer<typeof formSchema>;
|
||||
form.reset(formValues);
|
||||
}, [form, knowledgeDetails]);
|
||||
|
||||
return knowledgeDetails;
|
||||
};
|
||||
|
||||
export const useSelectKnowledgeDetailsLoading = () =>
|
||||
useIsFetching({ queryKey: ['fetchKnowledgeDetail'] }) > 0;
|
||||
|
||||
export const useRenameKnowledgeTag = () => {
|
||||
const [tag, setTag] = useState<string>('');
|
||||
const {
|
||||
visible: tagRenameVisible,
|
||||
hideModal: hideTagRenameModal,
|
||||
showModal: showFileRenameModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const handleShowTagRenameModal = useCallback(
|
||||
(record: string) => {
|
||||
setTag(record);
|
||||
showFileRenameModal();
|
||||
},
|
||||
[showFileRenameModal],
|
||||
);
|
||||
|
||||
return {
|
||||
initialName: tag,
|
||||
tagRenameVisible,
|
||||
hideTagRenameModal,
|
||||
showTagRenameModal: handleShowTagRenameModal,
|
||||
};
|
||||
};
|
||||
230
ragflow_web/src/pages/dataset/dataset-setting/index.tsx
Normal file
230
ragflow_web/src/pages/dataset/dataset-setting/index.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { DataFlowSelect } from '@/components/data-pipeline-select';
|
||||
import GraphRagItems from '@/components/parse-configuration/graph-rag-form-fields';
|
||||
import RaptorFormFields from '@/components/parse-configuration/raptor-form-fields';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Divider from '@/components/ui/divider';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { PermissionRole } from '@/constants/permission';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { TopTitle } from '../dataset-title';
|
||||
import {
|
||||
GenerateType,
|
||||
IGenerateLogButtonProps,
|
||||
} from '../dataset/generate-button/generate';
|
||||
import { ChunkMethodForm } from './chunk-method-form';
|
||||
import ChunkMethodLearnMore from './chunk-method-learn-more';
|
||||
import { MainContainer } from './configuration-form-container';
|
||||
import { ChunkMethodItem, ParseTypeItem } from './configuration/common-item';
|
||||
import { formSchema } from './form-schema';
|
||||
import { GeneralForm } from './general-form';
|
||||
import { useFetchKnowledgeConfigurationOnMount } from './hooks';
|
||||
import { SavingButton } from './saving-button';
|
||||
const enum DocumentType {
|
||||
DeepDOC = 'DeepDOC',
|
||||
PlainText = 'Plain Text',
|
||||
}
|
||||
|
||||
const initialEntityTypes = [
|
||||
'organization',
|
||||
'person',
|
||||
'geo',
|
||||
'event',
|
||||
'category',
|
||||
];
|
||||
|
||||
const enum MethodValue {
|
||||
General = 'general',
|
||||
Light = 'light',
|
||||
}
|
||||
|
||||
export default function DatasetSettings() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
parser_id: DocumentParserType.Naive,
|
||||
permission: PermissionRole.Me,
|
||||
parser_config: {
|
||||
layout_recognize: DocumentType.DeepDOC,
|
||||
chunk_token_num: 512,
|
||||
delimiter: `\n`,
|
||||
auto_keywords: 0,
|
||||
auto_questions: 0,
|
||||
html4excel: false,
|
||||
topn_tags: 3,
|
||||
toc_extraction: false,
|
||||
raptor: {
|
||||
use_raptor: true,
|
||||
max_token: 256,
|
||||
threshold: 0.1,
|
||||
max_cluster: 64,
|
||||
random_seed: 0,
|
||||
prompt: t('knowledgeConfiguration.promptText'),
|
||||
},
|
||||
graphrag: {
|
||||
use_graphrag: true,
|
||||
entity_types: initialEntityTypes,
|
||||
method: MethodValue.Light,
|
||||
},
|
||||
},
|
||||
pipeline_id: '',
|
||||
parseType: 1,
|
||||
pagerank: 0,
|
||||
},
|
||||
});
|
||||
const knowledgeDetails = useFetchKnowledgeConfigurationOnMount(form);
|
||||
// const [pipelineData, setPipelineData] = useState<IDataPipelineNodeProps>();
|
||||
const [graphRagGenerateData, setGraphRagGenerateData] =
|
||||
useState<IGenerateLogButtonProps>();
|
||||
const [raptorGenerateData, setRaptorGenerateData] =
|
||||
useState<IGenerateLogButtonProps>();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('🚀 ~ DatasetSettings ~ knowledgeDetails:', knowledgeDetails);
|
||||
if (knowledgeDetails) {
|
||||
// const data: IDataPipelineNodeProps = {
|
||||
// id: knowledgeDetails.pipeline_id,
|
||||
// name: knowledgeDetails.pipeline_name,
|
||||
// avatar: knowledgeDetails.pipeline_avatar,
|
||||
// linked: true,
|
||||
// };
|
||||
// setPipelineData(data);
|
||||
setGraphRagGenerateData({
|
||||
finish_at: knowledgeDetails.graphrag_task_finish_at,
|
||||
task_id: knowledgeDetails.graphrag_task_id,
|
||||
} as IGenerateLogButtonProps);
|
||||
setRaptorGenerateData({
|
||||
finish_at: knowledgeDetails.raptor_task_finish_at,
|
||||
task_id: knowledgeDetails.raptor_task_id,
|
||||
} as IGenerateLogButtonProps);
|
||||
form.setValue('parseType', knowledgeDetails.pipeline_id ? 2 : 1);
|
||||
form.setValue('pipeline_id', knowledgeDetails.pipeline_id || '');
|
||||
}
|
||||
}, [knowledgeDetails, form]);
|
||||
|
||||
async function onSubmit(data: z.infer<typeof formSchema>) {
|
||||
try {
|
||||
console.log('Form validation passed, submit data', data);
|
||||
} catch (error) {
|
||||
console.error('An error occurred during submission:', error);
|
||||
}
|
||||
}
|
||||
// const handleLinkOrEditSubmit = (
|
||||
// data: IDataPipelineSelectNode | undefined,
|
||||
// ) => {
|
||||
// console.log('🚀 ~ DatasetSettings ~ data:', data);
|
||||
// if (data) {
|
||||
// setPipelineData(data);
|
||||
// form.setValue('pipeline_id', data.id || '');
|
||||
// // form.setValue('pipeline_name', data.name || '');
|
||||
// // form.setValue('pipeline_avatar', data.avatar || '');
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleDeletePipelineTask = (type: GenerateType) => {
|
||||
if (type === GenerateType.KnowledgeGraph) {
|
||||
setGraphRagGenerateData({
|
||||
finish_at: '',
|
||||
task_id: '',
|
||||
} as IGenerateLogButtonProps);
|
||||
} else if (type === GenerateType.Raptor) {
|
||||
setRaptorGenerateData({
|
||||
finish_at: '',
|
||||
task_id: '',
|
||||
} as IGenerateLogButtonProps);
|
||||
}
|
||||
};
|
||||
|
||||
const parseType = useWatch({
|
||||
control: form.control,
|
||||
name: 'parseType',
|
||||
defaultValue: knowledgeDetails.pipeline_id ? 2 : 1,
|
||||
});
|
||||
const selectedTag = useWatch({
|
||||
name: 'parser_id',
|
||||
control: form.control,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (parseType === 1) {
|
||||
form.setValue('pipeline_id', '');
|
||||
}
|
||||
console.log('parseType', parseType);
|
||||
}, [parseType, form]);
|
||||
return (
|
||||
<section className="p-5 h-full flex flex-col">
|
||||
<TopTitle
|
||||
title={t('knowledgeDetails.configuration')}
|
||||
description={t('knowledgeConfiguration.titleDescription')}
|
||||
></TopTitle>
|
||||
<div className="flex gap-14 flex-1 min-h-0">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 ">
|
||||
<div className="w-[768px] h-[calc(100vh-240px)] pr-1 overflow-y-auto scrollbar-auto">
|
||||
<MainContainer className="text-text-secondary">
|
||||
<GeneralForm></GeneralForm>
|
||||
<Divider />
|
||||
|
||||
<GraphRagItems
|
||||
className="border-none p-0"
|
||||
data={graphRagGenerateData as IGenerateLogButtonProps}
|
||||
onDelete={() =>
|
||||
handleDeletePipelineTask(GenerateType.KnowledgeGraph)
|
||||
}
|
||||
></GraphRagItems>
|
||||
<Divider />
|
||||
<RaptorFormFields
|
||||
data={raptorGenerateData as IGenerateLogButtonProps}
|
||||
onDelete={() => handleDeletePipelineTask(GenerateType.Raptor)}
|
||||
></RaptorFormFields>
|
||||
<Divider />
|
||||
<ParseTypeItem line={1} />
|
||||
{parseType === 1 && (
|
||||
<ChunkMethodItem line={1}></ChunkMethodItem>
|
||||
)}
|
||||
{parseType === 2 && (
|
||||
<DataFlowSelect
|
||||
isMult={false}
|
||||
showToDataPipeline={true}
|
||||
formFieldName="pipeline_id"
|
||||
layout={FormLayout.Horizontal}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
{parseType === 1 && <ChunkMethodForm />}
|
||||
|
||||
{/* <LinkDataPipeline
|
||||
data={pipelineData}
|
||||
handleLinkOrEditSubmit={handleLinkOrEditSubmit}
|
||||
/> */}
|
||||
</MainContainer>
|
||||
</div>
|
||||
<div className="text-right items-center flex justify-end gap-3 w-[768px]">
|
||||
<Button
|
||||
type="reset"
|
||||
className="bg-transparent text-color-white hover:bg-transparent border-gray-500 border-[1px]"
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
{t('knowledgeConfiguration.cancel')}
|
||||
</Button>
|
||||
<SavingButton></SavingButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="flex-1">
|
||||
{parseType === 1 && <ChunkMethodLearnMore parserId={selectedTag} />}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { PermissionRole } from '@/constants/permission';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function PermissionFormField() {
|
||||
const { t } = useTranslation();
|
||||
const teamOptions = useMemo(() => {
|
||||
return Object.values(PermissionRole).map((x) => ({
|
||||
label: t('knowledgeConfiguration.' + x),
|
||||
value: x,
|
||||
}));
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name="permission"
|
||||
label={t('knowledgeConfiguration.permissions')}
|
||||
tooltip={t('knowledgeConfiguration.permissionsTip')}
|
||||
horizontal
|
||||
>
|
||||
<SelectWithSearch
|
||||
options={teamOptions}
|
||||
triggerClassName="w-3/4"
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ButtonLoading } from '@/components/ui/button';
|
||||
import { useUpdateKnowledge } from '@/hooks/use-knowledge-request';
|
||||
import { useMemo } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'umi';
|
||||
|
||||
export function GeneralSavingButton() {
|
||||
const form = useFormContext();
|
||||
const { saveKnowledgeConfiguration, loading: submitLoading } =
|
||||
useUpdateKnowledge();
|
||||
const { id: kb_id } = useParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const defaultValues = useMemo(
|
||||
() => form.formState.defaultValues ?? {},
|
||||
[form.formState.defaultValues],
|
||||
);
|
||||
const parser_id = defaultValues['parser_id'];
|
||||
|
||||
return (
|
||||
<ButtonLoading
|
||||
type="button"
|
||||
loading={submitLoading}
|
||||
onClick={() => {
|
||||
(async () => {
|
||||
let isValidate = await form.trigger('name');
|
||||
const { name, description, permission, avatar } = form.getValues();
|
||||
|
||||
if (isValidate) {
|
||||
saveKnowledgeConfiguration({
|
||||
kb_id,
|
||||
parser_id,
|
||||
name,
|
||||
description,
|
||||
avatar,
|
||||
permission,
|
||||
});
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
{t('knowledgeConfiguration.save')}
|
||||
</ButtonLoading>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavingButton() {
|
||||
const { saveKnowledgeConfiguration, loading: submitLoading } =
|
||||
useUpdateKnowledge();
|
||||
const form = useFormContext();
|
||||
const { id: kb_id } = useParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonLoading
|
||||
loading={submitLoading}
|
||||
onClick={() => {
|
||||
(async () => {
|
||||
try {
|
||||
let beValid = await form.formControl.trigger();
|
||||
if (beValid) {
|
||||
form.handleSubmit(async (values) => {
|
||||
console.log('saveKnowledgeConfiguration: ', values);
|
||||
delete values['parseType'];
|
||||
// delete values['avatar'];
|
||||
await saveKnowledgeConfiguration({
|
||||
kb_id,
|
||||
...values,
|
||||
});
|
||||
})();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
{t('knowledgeConfiguration.save')}
|
||||
</ButtonLoading>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { ArrowUpDown, Pencil, Trash2 } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useDeleteTag, useFetchTagList } from '@/hooks/knowledge-hooks';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRenameKnowledgeTag } from '../hooks';
|
||||
import { RenameDialog } from './rename-dialog';
|
||||
|
||||
export type ITag = {
|
||||
tag: string;
|
||||
frequency: number;
|
||||
};
|
||||
|
||||
export function TagTable() {
|
||||
const { t } = useTranslation();
|
||||
const { list } = useFetchTagList();
|
||||
const [tagList, setTagList] = useState<ITag[]>([]);
|
||||
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
|
||||
const { deleteTag } = useDeleteTag();
|
||||
|
||||
useEffect(() => {
|
||||
setTagList(list.map((x) => ({ tag: x[0], frequency: x[1] })));
|
||||
}, [list]);
|
||||
|
||||
const handleDeleteTag = useCallback(
|
||||
(tags: string[]) => () => {
|
||||
deleteTag(tags);
|
||||
},
|
||||
[deleteTag],
|
||||
);
|
||||
|
||||
const {
|
||||
showTagRenameModal,
|
||||
hideTagRenameModal,
|
||||
tagRenameVisible,
|
||||
initialName,
|
||||
} = useRenameKnowledgeTag();
|
||||
|
||||
const columns: ColumnDef<ITag>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'tag',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
{t('knowledgeConfiguration.tagName')}
|
||||
<ArrowUpDown />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const value: string = row.getValue('tag');
|
||||
return <div>{value}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'frequency',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
{t('knowledgeConfiguration.frequency')}
|
||||
<ArrowUpDown />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className="capitalize ">{row.getValue('frequency')}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableHiding: false,
|
||||
header: t('common.action'),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<Tooltip>
|
||||
<ConfirmDeleteDialog onOk={handleDeleteTag([row.original.tag])}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</ConfirmDeleteDialog>
|
||||
<TooltipContent>
|
||||
<p>{t('common.delete')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => showTagRenameModal(row.original.tag)}
|
||||
>
|
||||
<Pencil />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('common.rename')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: tagList,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedRowLength = table.getFilteredSelectedRowModel().rows.length;
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between py-4 ">
|
||||
<Input
|
||||
placeholder={t('knowledgeConfiguration.searchTags')}
|
||||
value={(table.getColumn('tag')?.getFilterValue() as string) ?? ''}
|
||||
onChange={(event) =>
|
||||
table.getColumn('tag')?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="w-1/2"
|
||||
/>
|
||||
{selectedRowLength > 0 && (
|
||||
<ConfirmDeleteDialog
|
||||
onOk={handleDeleteTag(
|
||||
table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((x) => x.original.tag),
|
||||
)}
|
||||
>
|
||||
<Button variant="outline" size="icon">
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
)}
|
||||
</div>
|
||||
<Table rootClassName="rounded-none border max-h-80 overflow-y-auto">
|
||||
<TableHeader className="bg-[#39393b]">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
{selectedRowLength} of {table.getFilteredRowModel().rows.length}{' '}
|
||||
row(s) selected.
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
{t('common.previousPage')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
{t('common.nextPage')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{tagRenameVisible && (
|
||||
<RenameDialog
|
||||
hideModal={hideTagRenameModal}
|
||||
initialName={initialName}
|
||||
></RenameDialog>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { LoadingButton } from '@/components/ui/loading-button';
|
||||
import { useTagIsRenaming } from '@/hooks/knowledge-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { TagRenameId } from '@/pages/add-knowledge/constant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RenameForm } from './rename-form';
|
||||
|
||||
export function RenameDialog({
|
||||
hideModal,
|
||||
initialName,
|
||||
}: IModalProps<any> & { initialName: string }) {
|
||||
const { t } = useTranslation();
|
||||
const loading = useTagIsRenaming();
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('common.rename')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<RenameForm
|
||||
initialName={initialName}
|
||||
hideModal={hideModal}
|
||||
></RenameForm>
|
||||
<DialogFooter>
|
||||
<LoadingButton type="submit" form={TagRenameId} loading={loading}>
|
||||
{t('common.save')}
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useRenameTag } from '@/hooks/knowledge-hooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { TagRenameId } from '@/pages/add-knowledge/constant';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function RenameForm({
|
||||
initialName,
|
||||
hideModal,
|
||||
}: IModalProps<any> & { initialName: string }) {
|
||||
const { t } = useTranslation();
|
||||
const FormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: t('common.namePlaceholder'),
|
||||
})
|
||||
.trim(),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { renameTag } = useRenameTag();
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
const ret = await renameTag({ fromTag: initialName, toTag: data.name });
|
||||
if (ret) {
|
||||
hideModal?.();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('name', initialName);
|
||||
}, [form, initialName]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
id={TagRenameId}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('common.namePlaceholder')}
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
40
ragflow_web/src/pages/dataset/dataset-setting/tag-tabs.tsx
Normal file
40
ragflow_web/src/pages/dataset/dataset-setting/tag-tabs.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Segmented } from 'antd';
|
||||
import { SegmentedLabeledOption } from 'antd/es/segmented';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TagTable } from './tag-table';
|
||||
import { TagWordCloud } from './tag-word-cloud';
|
||||
|
||||
enum TagType {
|
||||
Cloud = 'cloud',
|
||||
Table = 'table',
|
||||
}
|
||||
|
||||
const TagContentMap = {
|
||||
[TagType.Cloud]: <TagWordCloud></TagWordCloud>,
|
||||
[TagType.Table]: <TagTable></TagTable>,
|
||||
};
|
||||
|
||||
export function TagTabs() {
|
||||
const [value, setValue] = useState<TagType>(TagType.Cloud);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options: SegmentedLabeledOption[] = [TagType.Cloud, TagType.Table].map(
|
||||
(x) => ({
|
||||
label: t(`knowledgeConfiguration.tag${upperFirst(x)}`),
|
||||
value: x,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="mt-4">
|
||||
<Segmented
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={(val) => setValue(val as TagType)}
|
||||
/>
|
||||
{TagContentMap[value]}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useFetchTagList } from '@/hooks/knowledge-hooks';
|
||||
import { Chart } from '@antv/g2';
|
||||
import { sumBy } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
export function TagWordCloud() {
|
||||
const domRef = useRef<HTMLDivElement>(null);
|
||||
let chartRef = useRef<Chart>();
|
||||
const { list } = useFetchTagList();
|
||||
|
||||
const { list: tagList } = useMemo(() => {
|
||||
const nextList = list.sort((a, b) => b[1] - a[1]).slice(0, 256);
|
||||
|
||||
return {
|
||||
list: nextList.map((x) => ({ text: x[0], value: x[1], name: x[0] })),
|
||||
sumValue: sumBy(nextList, (x: [string, number]) => x[1]),
|
||||
length: nextList.length,
|
||||
};
|
||||
}, [list]);
|
||||
|
||||
const renderWordCloud = useCallback(() => {
|
||||
if (domRef.current) {
|
||||
chartRef.current = new Chart({ container: domRef.current });
|
||||
|
||||
chartRef.current.options({
|
||||
type: 'wordCloud',
|
||||
autoFit: true,
|
||||
layout: {
|
||||
fontSize: [10, 50],
|
||||
// fontSize: (d: any) => {
|
||||
// if (d.value) {
|
||||
// return (d.value / sumValue) * 100 * (length / 10);
|
||||
// }
|
||||
// return 0;
|
||||
// },
|
||||
},
|
||||
data: {
|
||||
type: 'inline',
|
||||
value: tagList,
|
||||
},
|
||||
encode: { color: 'text' },
|
||||
legend: false,
|
||||
tooltip: {
|
||||
title: 'name', // title
|
||||
items: ['value'], // data item
|
||||
},
|
||||
});
|
||||
|
||||
chartRef.current.render();
|
||||
}
|
||||
}, [tagList]);
|
||||
|
||||
useEffect(() => {
|
||||
renderWordCloud();
|
||||
|
||||
return () => {
|
||||
chartRef.current?.destroy();
|
||||
};
|
||||
}, [renderWordCloud]);
|
||||
|
||||
return <div ref={domRef} className="w-full h-[38vh]"></div>;
|
||||
}
|
||||
20
ragflow_web/src/pages/dataset/dataset-setting/utils.ts
Normal file
20
ragflow_web/src/pages/dataset/dataset-setting/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
const getImageName = (prefix: string, length: number) =>
|
||||
new Array(length)
|
||||
.fill(0)
|
||||
.map((x, idx) => `chunk-method/${prefix}-0${idx + 1}`);
|
||||
|
||||
export const ImageMap = {
|
||||
book: getImageName('book', 4),
|
||||
laws: getImageName('law', 2),
|
||||
manual: getImageName('manual', 4),
|
||||
picture: getImageName('media', 2),
|
||||
naive: getImageName('naive', 2),
|
||||
paper: getImageName('paper', 2),
|
||||
presentation: getImageName('presentation', 2),
|
||||
qa: getImageName('qa', 2),
|
||||
resume: getImageName('resume', 2),
|
||||
table: getImageName('table', 2),
|
||||
one: getImageName('one', 2),
|
||||
knowledge_graph: getImageName('knowledge-graph', 2),
|
||||
tag: getImageName('tag', 2),
|
||||
};
|
||||
15
ragflow_web/src/pages/dataset/dataset-title.tsx
Normal file
15
ragflow_web/src/pages/dataset/dataset-title.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type TopTitleProps = {
|
||||
title: ReactNode;
|
||||
description: ReactNode;
|
||||
};
|
||||
|
||||
export function TopTitle({ title, description }: TopTitleProps) {
|
||||
return (
|
||||
<div className="pb-5">
|
||||
<div className="text-2xl font-semibold">{title}</div>
|
||||
<p className="text-text-secondary pt-2">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
ragflow_web/src/pages/dataset/dataset/constant.ts
Normal file
23
ragflow_web/src/pages/dataset/dataset/constant.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { RunningStatus } from '@/constants/knowledge';
|
||||
|
||||
export const RunningStatusMap = {
|
||||
[RunningStatus.UNSTART]: {
|
||||
label: 'UNSTART',
|
||||
color: 'rgba(var(--accent-primary))',
|
||||
},
|
||||
[RunningStatus.RUNNING]: {
|
||||
label: 'Parsing',
|
||||
color: 'var(--team-member)',
|
||||
},
|
||||
[RunningStatus.CANCEL]: {
|
||||
label: 'CANCEL',
|
||||
color: 'rgba(var(--state-warning))',
|
||||
},
|
||||
[RunningStatus.DONE]: {
|
||||
label: 'SUCCESS',
|
||||
color: 'rgba(var(--state-success))',
|
||||
},
|
||||
[RunningStatus.FAIL]: { label: 'FAIL', color: 'rgba(var(--state-error))' },
|
||||
};
|
||||
|
||||
export * from '@/constants/knowledge';
|
||||
118
ragflow_web/src/pages/dataset/dataset/dataset-action-cell.tsx
Normal file
118
ragflow_web/src/pages/dataset/dataset/dataset-action-cell.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/hover-card';
|
||||
import { DocumentType } from '@/constants/knowledge';
|
||||
import { useRemoveDocument } from '@/hooks/use-document-request';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { formatFileSize } from '@/utils/common-util';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { downloadDocument } from '@/utils/file-util';
|
||||
import { Download, Eye, PenLine, Trash2 } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { UseRenameDocumentShowType } from './use-rename-document';
|
||||
import { isParserRunning } from './utils';
|
||||
|
||||
const Fields = ['name', 'size', 'type', 'create_time', 'update_time'];
|
||||
|
||||
const FunctionMap = {
|
||||
size: formatFileSize,
|
||||
create_time: formatDate,
|
||||
update_time: formatDate,
|
||||
};
|
||||
|
||||
export function DatasetActionCell({
|
||||
record,
|
||||
showRenameModal,
|
||||
}: { record: IDocumentInfo } & UseRenameDocumentShowType) {
|
||||
const { id, run, type } = record;
|
||||
const isRunning = isParserRunning(run);
|
||||
const isVirtualDocument = type === DocumentType.Virtual;
|
||||
|
||||
const { removeDocument } = useRemoveDocument();
|
||||
|
||||
const onDownloadDocument = useCallback(() => {
|
||||
downloadDocument({
|
||||
id,
|
||||
filename: record.name,
|
||||
});
|
||||
}, [id, record.name]);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
removeDocument(id);
|
||||
}, [id, removeDocument]);
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
showRenameModal(record);
|
||||
}, [record, showRenameModal]);
|
||||
|
||||
return (
|
||||
<section className="flex gap-4 items-center text-text-sub-title-invert opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="border-none hover:bg-bg-card text-text-primary"
|
||||
size={'sm'}
|
||||
disabled={isRunning}
|
||||
onClick={handleRename}
|
||||
>
|
||||
<PenLine />
|
||||
</Button>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="border-none hover:bg-bg-card text-text-primary"
|
||||
disabled={isRunning}
|
||||
size={'sm'}
|
||||
>
|
||||
<Eye />
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-[40vw] max-h-[40vh] overflow-auto">
|
||||
<ul className="space-y-2">
|
||||
{Object.entries(record)
|
||||
.filter(([key]) => Fields.some((x) => x === key))
|
||||
|
||||
.map(([key, value], idx) => {
|
||||
return (
|
||||
<li key={idx} className="flex gap-2">
|
||||
{key}:
|
||||
<div>
|
||||
{key in FunctionMap
|
||||
? FunctionMap[key as keyof typeof FunctionMap](value)
|
||||
: value}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
{isVirtualDocument || (
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="border-none hover:bg-bg-card text-text-primary"
|
||||
onClick={onDownloadDocument}
|
||||
disabled={isRunning}
|
||||
size={'sm'}
|
||||
>
|
||||
<Download />
|
||||
</Button>
|
||||
)}
|
||||
<ConfirmDeleteDialog onOk={handleRemove}>
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="border-none hover:bg-bg-card text-text-primary"
|
||||
size={'sm'}
|
||||
disabled={isRunning}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
226
ragflow_web/src/pages/dataset/dataset/dataset-table.tsx
Normal file
226
ragflow_web/src/pages/dataset/dataset/dataset-table.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ChunkMethodDialog } from '@/components/chunk-method-dialog';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection';
|
||||
import { useFetchDocumentList } from '@/hooks/use-document-request';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { t } from 'i18next';
|
||||
import { pick } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import ProcessLogModal from '../process-log-modal';
|
||||
import { useShowLog } from './hooks';
|
||||
import { SetMetaDialog } from './set-meta-dialog';
|
||||
import { useChangeDocumentParser } from './use-change-document-parser';
|
||||
import { useDatasetTableColumns } from './use-dataset-table-columns';
|
||||
import { useRenameDocument } from './use-rename-document';
|
||||
import { useSaveMeta } from './use-save-meta';
|
||||
|
||||
export type DatasetTableProps = Pick<
|
||||
ReturnType<typeof useFetchDocumentList>,
|
||||
'documents' | 'setPagination' | 'pagination' | 'loading'
|
||||
> &
|
||||
Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'>;
|
||||
|
||||
export function DatasetTable({
|
||||
documents,
|
||||
pagination,
|
||||
setPagination,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
}: DatasetTableProps) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
|
||||
const {
|
||||
changeParserLoading,
|
||||
onChangeParserOk,
|
||||
changeParserVisible,
|
||||
hideChangeParserModal,
|
||||
showChangeParserModal,
|
||||
changeParserRecord,
|
||||
} = useChangeDocumentParser();
|
||||
|
||||
const {
|
||||
renameLoading,
|
||||
onRenameOk,
|
||||
renameVisible,
|
||||
hideRenameModal,
|
||||
showRenameModal,
|
||||
initialName,
|
||||
} = useRenameDocument();
|
||||
|
||||
const {
|
||||
showSetMetaModal,
|
||||
hideSetMetaModal,
|
||||
setMetaVisible,
|
||||
setMetaLoading,
|
||||
onSetMetaModalOk,
|
||||
metaRecord,
|
||||
} = useSaveMeta();
|
||||
const { showLog, logInfo, logVisible, hideLog } = useShowLog(documents);
|
||||
|
||||
const columns = useDatasetTableColumns({
|
||||
showChangeParserModal,
|
||||
showRenameModal,
|
||||
showSetMetaModal,
|
||||
showLog,
|
||||
});
|
||||
|
||||
const currentPagination = useMemo(() => {
|
||||
return {
|
||||
pageIndex: (pagination.current || 1) - 1,
|
||||
pageSize: pagination.pageSize || 10,
|
||||
};
|
||||
}, [pagination]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: documents,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
manualPagination: true, //we're doing manual "server-side" pagination
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
pagination: currentPagination,
|
||||
},
|
||||
rowCount: pagination.total ?? 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Table rootClassName="max-h-[calc(100vh-222px)]">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="relative">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className="group"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={cell.column.columnDef.meta?.cellClassName}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex items-center justify-end py-4 absolute bottom-3 right-3">
|
||||
<div className="space-x-2">
|
||||
<RAGFlowPagination
|
||||
{...pick(pagination, 'current', 'pageSize')}
|
||||
total={pagination.total}
|
||||
onChange={(page, pageSize) => {
|
||||
setPagination({ page, pageSize });
|
||||
}}
|
||||
></RAGFlowPagination>
|
||||
</div>
|
||||
</div>
|
||||
{changeParserVisible && (
|
||||
<ChunkMethodDialog
|
||||
documentId={changeParserRecord.id}
|
||||
parserId={changeParserRecord.parser_id}
|
||||
pipelineId={changeParserRecord.pipeline_id}
|
||||
parserConfig={changeParserRecord.parser_config}
|
||||
documentExtension={getExtension(changeParserRecord.name)}
|
||||
onOk={onChangeParserOk}
|
||||
visible={changeParserVisible}
|
||||
hideModal={hideChangeParserModal}
|
||||
loading={changeParserLoading}
|
||||
></ChunkMethodDialog>
|
||||
)}
|
||||
|
||||
{renameVisible && (
|
||||
<RenameDialog
|
||||
visible={renameVisible}
|
||||
onOk={onRenameOk}
|
||||
loading={renameLoading}
|
||||
hideModal={hideRenameModal}
|
||||
initialName={initialName}
|
||||
></RenameDialog>
|
||||
)}
|
||||
|
||||
{setMetaVisible && (
|
||||
<SetMetaDialog
|
||||
hideModal={hideSetMetaModal}
|
||||
loading={setMetaLoading}
|
||||
onOk={onSetMetaModalOk}
|
||||
initialMetaData={metaRecord.meta_fields}
|
||||
></SetMetaDialog>
|
||||
)}
|
||||
{logVisible && (
|
||||
<ProcessLogModal
|
||||
title={t('knowledgeDetails.fileLogs')}
|
||||
visible={logVisible}
|
||||
onCancel={() => hideLog()}
|
||||
logInfo={logInfo}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import { IconFontFill } from '@/components/icon-font';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toFixed } from '@/utils/common-util';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { UseMutateAsyncFunction } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { lowerFirst } from 'lodash';
|
||||
import { CirclePause, Trash2, WandSparkles } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProcessingType } from '../../dataset-overview/dataset-common';
|
||||
import { replaceText } from '../../process-log-modal';
|
||||
import {
|
||||
ITraceInfo,
|
||||
generateStatus,
|
||||
useDatasetGenerate,
|
||||
useTraceGenerate,
|
||||
useUnBindTask,
|
||||
} from './hook';
|
||||
export enum GenerateType {
|
||||
KnowledgeGraph = 'KnowledgeGraph',
|
||||
Raptor = 'Raptor',
|
||||
}
|
||||
export const GenerateTypeMap = {
|
||||
[GenerateType.KnowledgeGraph]: ProcessingType.knowledgeGraph,
|
||||
[GenerateType.Raptor]: ProcessingType.raptor,
|
||||
};
|
||||
const MenuItem: React.FC<{
|
||||
name: GenerateType;
|
||||
data: ITraceInfo;
|
||||
pauseGenerate: ({
|
||||
task_id,
|
||||
type,
|
||||
}: {
|
||||
task_id: string;
|
||||
type: GenerateType;
|
||||
}) => void;
|
||||
runGenerate: UseMutateAsyncFunction<
|
||||
any,
|
||||
Error,
|
||||
{
|
||||
type: GenerateType;
|
||||
},
|
||||
unknown
|
||||
>;
|
||||
}> = ({ name: type, runGenerate, data, pauseGenerate }) => {
|
||||
const iconKeyMap = {
|
||||
KnowledgeGraph: 'knowledgegraph',
|
||||
Raptor: 'dataflow-01',
|
||||
};
|
||||
const status = useMemo(() => {
|
||||
if (!data) {
|
||||
return generateStatus.start;
|
||||
}
|
||||
if (data.progress >= 1) {
|
||||
return generateStatus.completed;
|
||||
} else if (!data.progress && data.progress !== 0) {
|
||||
return generateStatus.start;
|
||||
} else if (data.progress < 0) {
|
||||
return generateStatus.failed;
|
||||
} else if (data.progress < 1) {
|
||||
return generateStatus.running;
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const percent =
|
||||
status === generateStatus.failed
|
||||
? 100
|
||||
: status === generateStatus.running
|
||||
? data.progress * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
'border cursor-pointer p-2 rounded-md focus:bg-transparent',
|
||||
{
|
||||
'hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)] focus:bg-[rgba(59,160,92,0.1)]':
|
||||
status === generateStatus.start ||
|
||||
status === generateStatus.completed,
|
||||
'hover:border-border hover:bg-[rgba(59,160,92,0)] focus:bg-[rgba(59,160,92,0)]':
|
||||
status !== generateStatus.start &&
|
||||
status !== generateStatus.completed,
|
||||
},
|
||||
)}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-start gap-2 flex-col w-full"
|
||||
onClick={() => {
|
||||
if (
|
||||
status === generateStatus.start ||
|
||||
status === generateStatus.completed
|
||||
) {
|
||||
runGenerate({ type });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-start text-text-primary items-center gap-2">
|
||||
<IconFontFill
|
||||
name={iconKeyMap[type]}
|
||||
className="text-accent-primary"
|
||||
/>
|
||||
{t(`knowledgeDetails.${lowerFirst(type)}`)}
|
||||
</div>
|
||||
{(status === generateStatus.start ||
|
||||
status === generateStatus.completed) && (
|
||||
<div className="text-text-secondary text-sm">
|
||||
{t(`knowledgeDetails.generate${type}`)}
|
||||
</div>
|
||||
)}
|
||||
{(status === generateStatus.running ||
|
||||
status === generateStatus.failed) && (
|
||||
<div className="flex justify-between items-center w-full px-2.5 py-1">
|
||||
<div
|
||||
className={cn(' bg-border-button h-1 rounded-full', {
|
||||
'w-[calc(100%-100px)]': status === generateStatus.running,
|
||||
'w-[calc(100%-50px)]': status === generateStatus.failed,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cn('h-1 rounded-full', {
|
||||
'bg-state-error': status === generateStatus.failed,
|
||||
'bg-accent-primary': status === generateStatus.running,
|
||||
})}
|
||||
style={{ width: `${toFixed(percent)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
{status === generateStatus.running && (
|
||||
<span>{(toFixed(percent) as string) + '%'}</span>
|
||||
)}
|
||||
{status === generateStatus.failed && (
|
||||
<span
|
||||
className="text-state-error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
runGenerate({ type });
|
||||
}}
|
||||
>
|
||||
<IconFontFill name="reparse" className="text-accent-primary" />
|
||||
</span>
|
||||
)}
|
||||
{status !== generateStatus.failed && (
|
||||
<span
|
||||
className="text-state-error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
pauseGenerate({ task_id: data.id, type });
|
||||
}}
|
||||
>
|
||||
<CirclePause />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{status !== generateStatus.start &&
|
||||
status !== generateStatus.completed && (
|
||||
<div className="w-full whitespace-pre-line text-wrap rounded-lg h-fit max-h-[350px] overflow-y-auto scrollbar-auto px-2.5 py-1">
|
||||
{replaceText(data?.progress_msg || '')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
type GenerateProps = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
const Generate: React.FC<GenerateProps> = (props) => {
|
||||
const { disabled = false } = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
const { graphRunData, raptorRunData } = useTraceGenerate({ open });
|
||||
const { runGenerate, pauseGenerate } = useDatasetGenerate();
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
setOpen(isOpen);
|
||||
console.log('Dropdown is now', isOpen ? 'open' : 'closed');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="generate">
|
||||
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
<div className={cn({ 'cursor-not-allowed': disabled })}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
variant={'transparent'}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
handleOpenChange(!open);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<WandSparkles className="mr-2 size-4" />
|
||||
{t('knowledgeDetails.generate')}
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[380px] p-5 flex flex-col gap-2 ">
|
||||
{Object.values(GenerateType).map((name) => {
|
||||
const data = (
|
||||
name === GenerateType.KnowledgeGraph
|
||||
? graphRunData
|
||||
: raptorRunData
|
||||
) as ITraceInfo;
|
||||
console.log(
|
||||
name,
|
||||
'data',
|
||||
data,
|
||||
!data || (!data.progress && data.progress !== 0),
|
||||
);
|
||||
return (
|
||||
<div key={name}>
|
||||
<MenuItem
|
||||
name={name}
|
||||
runGenerate={runGenerate}
|
||||
data={data}
|
||||
pauseGenerate={pauseGenerate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Generate;
|
||||
|
||||
export type IGenerateLogButtonProps = {
|
||||
finish_at: string;
|
||||
task_id: string;
|
||||
};
|
||||
|
||||
export type IGenerateLogProps = IGenerateLogButtonProps & {
|
||||
id?: string;
|
||||
status: 0 | 1;
|
||||
message?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
type?: GenerateType;
|
||||
className?: string;
|
||||
onDelete?: () => void;
|
||||
};
|
||||
export const GenerateLogButton = (props: IGenerateLogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { message, finish_at, type, onDelete } = props;
|
||||
|
||||
const { handleUnbindTask } = useUnBindTask();
|
||||
|
||||
const handleDeleteFunc = async () => {
|
||||
const data = await handleUnbindTask({
|
||||
type: GenerateTypeMap[type as GenerateType],
|
||||
});
|
||||
Modal.destroy();
|
||||
console.log('handleUnbindTask', data);
|
||||
if (data.code === 0) {
|
||||
onDelete?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
Modal.show({
|
||||
visible: true,
|
||||
className: '!w-[560px]',
|
||||
title:
|
||||
t('common.delete') +
|
||||
' ' +
|
||||
(type === GenerateType.KnowledgeGraph
|
||||
? t('knowledgeDetails.knowledgeGraph')
|
||||
: t('knowledgeDetails.raptor')),
|
||||
children: (
|
||||
<div
|
||||
className="text-sm text-text-secondary"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('knowledgeConfiguration.deleteGenerateModalContent', {
|
||||
type:
|
||||
type === GenerateType.KnowledgeGraph
|
||||
? t('knowledgeDetails.knowledgeGraph')
|
||||
: t('knowledgeDetails.raptor'),
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
),
|
||||
onVisibleChange: () => {
|
||||
Modal.destroy();
|
||||
},
|
||||
footer: (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={'outline'}
|
||||
onClick={() => Modal.destroy()}
|
||||
>
|
||||
{t('dataflowParser.changeStepModalCancelText')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={'secondary'}
|
||||
className="!bg-state-error text-text-primary"
|
||||
onClick={() => {
|
||||
handleDeleteFunc();
|
||||
}}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex bg-bg-card rounded-md py-1 px-3', props.className)}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
{finish_at && (
|
||||
<>
|
||||
<div>
|
||||
{message || t('knowledgeDetails.generatedOn')}
|
||||
{formatDate(finish_at)}
|
||||
</div>
|
||||
<Trash2
|
||||
size={14}
|
||||
className="cursor-pointer"
|
||||
onClick={(e) => {
|
||||
console.log('delete');
|
||||
handleDelete();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!finish_at && <div>{t('knowledgeDetails.notGenerated')}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
179
ragflow_web/src/pages/dataset/dataset/generate-button/hook.ts
Normal file
179
ragflow_web/src/pages/dataset/dataset/generate-button/hook.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import message from '@/components/ui/message';
|
||||
import agentService from '@/services/agent-service';
|
||||
import kbService, { deletePipelineTask } from '@/services/knowledge-service';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'umi';
|
||||
import { ProcessingType } from '../../dataset-overview/dataset-common';
|
||||
import { GenerateType, GenerateTypeMap } from './generate';
|
||||
export const generateStatus = {
|
||||
running: 'running',
|
||||
completed: 'completed',
|
||||
start: 'start',
|
||||
failed: 'failed',
|
||||
};
|
||||
|
||||
enum DatasetKey {
|
||||
generate = 'generate',
|
||||
pauseGenerate = 'pauseGenerate',
|
||||
}
|
||||
|
||||
export interface ITraceInfo {
|
||||
begin_at: string;
|
||||
chunk_ids: string;
|
||||
create_date: string;
|
||||
create_time: number;
|
||||
digest: string;
|
||||
doc_id: string;
|
||||
from_page: number;
|
||||
id: string;
|
||||
priority: number;
|
||||
process_duration: number;
|
||||
progress: number;
|
||||
progress_msg: string;
|
||||
retry_count: number;
|
||||
task_type: string;
|
||||
to_page: number;
|
||||
update_date: string;
|
||||
update_time: number;
|
||||
}
|
||||
|
||||
export const useTraceGenerate = ({ open }: { open: boolean }) => {
|
||||
const { id } = useParams();
|
||||
const [isLoopGraphRun, setLoopGraphRun] = useState(false);
|
||||
const [isLoopRaptorRun, setLoopRaptorRun] = useState(false);
|
||||
const { data: graphRunData, isFetching: graphRunloading } =
|
||||
useQuery<ITraceInfo>({
|
||||
queryKey: [GenerateType.KnowledgeGraph, id, open],
|
||||
// initialData: {},
|
||||
gcTime: 0,
|
||||
refetchInterval: isLoopGraphRun ? 5000 : false,
|
||||
retry: 3,
|
||||
retryDelay: 1000,
|
||||
enabled: open,
|
||||
queryFn: async () => {
|
||||
const { data } = await kbService.traceGraphRag({
|
||||
kb_id: id,
|
||||
});
|
||||
return data?.data || {};
|
||||
},
|
||||
});
|
||||
|
||||
const { data: raptorRunData, isFetching: raptorRunloading } =
|
||||
useQuery<ITraceInfo>({
|
||||
queryKey: [GenerateType.Raptor, id, open],
|
||||
// initialData: {},
|
||||
gcTime: 0,
|
||||
refetchInterval: isLoopRaptorRun ? 5000 : false,
|
||||
retry: 3,
|
||||
retryDelay: 1000,
|
||||
enabled: open,
|
||||
queryFn: async () => {
|
||||
const { data } = await kbService.traceRaptor({
|
||||
kb_id: id,
|
||||
});
|
||||
return data?.data || {};
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setLoopGraphRun(
|
||||
!!(
|
||||
(graphRunData?.progress || graphRunData?.progress === 0) &&
|
||||
graphRunData?.progress < 1 &&
|
||||
graphRunData?.progress >= 0
|
||||
),
|
||||
);
|
||||
}, [graphRunData?.progress]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoopRaptorRun(
|
||||
!!(
|
||||
(raptorRunData?.progress || raptorRunData?.progress === 0) &&
|
||||
raptorRunData?.progress < 1 &&
|
||||
raptorRunData?.progress >= 0
|
||||
),
|
||||
);
|
||||
}, [raptorRunData?.progress]);
|
||||
return {
|
||||
graphRunData,
|
||||
graphRunloading,
|
||||
raptorRunData,
|
||||
raptorRunloading,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUnBindTask = () => {
|
||||
const { id } = useParams();
|
||||
const { mutateAsync: handleUnbindTask } = useMutation({
|
||||
mutationKey: [DatasetKey.pauseGenerate],
|
||||
mutationFn: async ({ type }: { type: ProcessingType }) => {
|
||||
const { data } = await deletePipelineTask({ kb_id: id as string, type });
|
||||
if (data.code === 0) {
|
||||
message.success(t('message.operated'));
|
||||
// queryClient.invalidateQueries({
|
||||
// queryKey: [type],
|
||||
// });
|
||||
}
|
||||
return data;
|
||||
},
|
||||
});
|
||||
return { handleUnbindTask };
|
||||
};
|
||||
export const useDatasetGenerate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { id } = useParams();
|
||||
const { handleUnbindTask } = useUnBindTask();
|
||||
const {
|
||||
data,
|
||||
isPending: loading,
|
||||
mutateAsync,
|
||||
} = useMutation({
|
||||
mutationKey: [DatasetKey.generate],
|
||||
mutationFn: async ({ type }: { type: GenerateType }) => {
|
||||
const func =
|
||||
type === GenerateType.KnowledgeGraph
|
||||
? kbService.runGraphRag
|
||||
: kbService.runRaptor;
|
||||
const { data } = await func({
|
||||
kb_id: id,
|
||||
});
|
||||
if (data.code === 0) {
|
||||
message.success(t('message.operated'));
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [type],
|
||||
});
|
||||
}
|
||||
return data;
|
||||
},
|
||||
});
|
||||
// const pauseGenerate = useCallback(() => {
|
||||
// // TODO: pause generate
|
||||
// console.log('pause generate');
|
||||
// }, []);
|
||||
const { mutateAsync: pauseGenerate } = useMutation({
|
||||
mutationKey: [DatasetKey.pauseGenerate],
|
||||
mutationFn: async ({
|
||||
task_id,
|
||||
type,
|
||||
}: {
|
||||
task_id: string;
|
||||
type: GenerateType;
|
||||
}) => {
|
||||
const { data } = await agentService.cancelDataflow(task_id);
|
||||
|
||||
const unbindData = await handleUnbindTask({
|
||||
type: GenerateTypeMap[type as GenerateType],
|
||||
});
|
||||
if (data.code === 0 && unbindData.code === 0) {
|
||||
// message.success(t('message.operated'));
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [type],
|
||||
});
|
||||
}
|
||||
return data;
|
||||
},
|
||||
});
|
||||
return { runGenerate: mutateAsync, pauseGenerate, data, loading };
|
||||
};
|
||||
105
ragflow_web/src/pages/dataset/dataset/hooks.ts
Normal file
105
ragflow_web/src/pages/dataset/dataset/hooks.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useNextWebCrawl } from '@/hooks/document-hooks';
|
||||
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { formatDate, formatSecondsToHumanReadable } from '@/utils/date';
|
||||
import { formatBytes } from '@/utils/file-util';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'umi';
|
||||
import { ILogInfo } from '../process-log-modal';
|
||||
import { RunningStatus } from './constant';
|
||||
|
||||
export const useNavigateToOtherPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { knowledgeId } = useGetKnowledgeSearchParams();
|
||||
|
||||
const linkToUploadPage = useCallback(() => {
|
||||
navigate(`/knowledge/dataset/upload?id=${knowledgeId}`);
|
||||
}, [navigate, knowledgeId]);
|
||||
|
||||
const toChunk = useCallback((id: string) => {}, []);
|
||||
|
||||
return { linkToUploadPage, toChunk };
|
||||
};
|
||||
|
||||
export const useGetRowSelection = () => {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (newSelectedRowKeys: React.Key[]) => {
|
||||
setSelectedRowKeys(newSelectedRowKeys);
|
||||
},
|
||||
};
|
||||
|
||||
return rowSelection;
|
||||
};
|
||||
|
||||
export const useHandleWebCrawl = () => {
|
||||
const {
|
||||
visible: webCrawlUploadVisible,
|
||||
hideModal: hideWebCrawlUploadModal,
|
||||
showModal: showWebCrawlUploadModal,
|
||||
} = useSetModalState();
|
||||
const { webCrawl, loading } = useNextWebCrawl();
|
||||
|
||||
const onWebCrawlUploadOk = useCallback(
|
||||
async (name: string, url: string) => {
|
||||
const ret = await webCrawl({ name, url });
|
||||
if (ret === 0) {
|
||||
hideWebCrawlUploadModal();
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
[webCrawl, hideWebCrawlUploadModal],
|
||||
);
|
||||
|
||||
return {
|
||||
webCrawlUploadLoading: loading,
|
||||
onWebCrawlUploadOk,
|
||||
webCrawlUploadVisible,
|
||||
hideWebCrawlUploadModal,
|
||||
showWebCrawlUploadModal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useShowLog = (documents: IDocumentInfo[]) => {
|
||||
const { showModal, hideModal, visible } = useSetModalState();
|
||||
const [record, setRecord] = useState<IDocumentInfo>();
|
||||
const logInfo = useMemo(() => {
|
||||
const findRecord = documents.find(
|
||||
(item: IDocumentInfo) => item.id === record?.id,
|
||||
);
|
||||
let log: ILogInfo = {
|
||||
taskId: record?.id,
|
||||
fileName: record?.name || '-',
|
||||
details: record?.progress_msg || '-',
|
||||
};
|
||||
if (findRecord) {
|
||||
log = {
|
||||
fileType: findRecord?.suffix,
|
||||
uploadedBy: findRecord?.nickname,
|
||||
fileName: findRecord?.name,
|
||||
uploadDate: formatDate(findRecord.create_date),
|
||||
fileSize: formatBytes(findRecord.size || 0),
|
||||
processBeginAt: formatDate(findRecord.process_begin_at),
|
||||
chunkNumber: findRecord.chunk_num,
|
||||
duration: formatSecondsToHumanReadable(
|
||||
findRecord.process_duration || 0,
|
||||
),
|
||||
status: findRecord.run as RunningStatus,
|
||||
details: findRecord.progress_msg,
|
||||
};
|
||||
}
|
||||
return log;
|
||||
}, [record, documents]);
|
||||
const showLog = useCallback(
|
||||
(data: IDocumentInfo) => {
|
||||
setRecord(data);
|
||||
showModal();
|
||||
},
|
||||
[showModal],
|
||||
);
|
||||
return { showLog, hideLog: hideModal, logVisible: visible, logInfo };
|
||||
};
|
||||
143
ragflow_web/src/pages/dataset/dataset/index.tsx
Normal file
143
ragflow_web/src/pages/dataset/dataset/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { BulkOperateBar } from '@/components/bulk-operate-bar';
|
||||
import { FileUploadDialog } from '@/components/file-upload-dialog';
|
||||
import ListFilterBar from '@/components/list-filter-bar';
|
||||
import { RenameDialog } from '@/components/rename-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useRowSelection } from '@/hooks/logic-hooks/use-row-selection';
|
||||
import { useFetchDocumentList } from '@/hooks/use-document-request';
|
||||
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DatasetTable } from './dataset-table';
|
||||
import Generate from './generate-button/generate';
|
||||
import { useBulkOperateDataset } from './use-bulk-operate-dataset';
|
||||
import { useCreateEmptyDocument } from './use-create-empty-document';
|
||||
import { useSelectDatasetFilters } from './use-select-filters';
|
||||
import { useHandleUploadDocument } from './use-upload-document';
|
||||
|
||||
export default function Dataset() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
documentUploadVisible,
|
||||
hideDocumentUploadModal,
|
||||
showDocumentUploadModal,
|
||||
onDocumentUploadOk,
|
||||
documentUploadLoading,
|
||||
} = useHandleUploadDocument();
|
||||
|
||||
const {
|
||||
searchString,
|
||||
documents,
|
||||
pagination,
|
||||
handleInputChange,
|
||||
setPagination,
|
||||
filterValue,
|
||||
handleFilterSubmit,
|
||||
loading,
|
||||
} = useFetchDocumentList();
|
||||
|
||||
const refreshCount = useMemo(() => {
|
||||
return documents.findIndex((doc) => doc.run === '1') + documents.length;
|
||||
}, [documents]);
|
||||
|
||||
const { data: dataSetData } = useFetchKnowledgeBaseConfiguration({
|
||||
refreshCount,
|
||||
});
|
||||
const { filters, onOpenChange } = useSelectDatasetFilters();
|
||||
|
||||
const {
|
||||
createLoading,
|
||||
onCreateOk,
|
||||
createVisible,
|
||||
hideCreateModal,
|
||||
showCreateModal,
|
||||
} = useCreateEmptyDocument();
|
||||
|
||||
const { rowSelection, rowSelectionIsEmpty, setRowSelection, selectedCount } =
|
||||
useRowSelection();
|
||||
|
||||
const { list } = useBulkOperateDataset({
|
||||
documents,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="absolute top-4 right-5">
|
||||
<Generate disabled={!(dataSetData.chunk_num > 0)} />
|
||||
</div>
|
||||
<section className="p-5 min-w-[880px]">
|
||||
<ListFilterBar
|
||||
title="Dataset"
|
||||
onSearchChange={handleInputChange}
|
||||
searchString={searchString}
|
||||
value={filterValue}
|
||||
onChange={handleFilterSubmit}
|
||||
onOpenChange={onOpenChange}
|
||||
filters={filters}
|
||||
leftPanel={
|
||||
<div className="items-start">
|
||||
<div className="pb-1">{t('knowledgeDetails.subbarFiles')}</div>
|
||||
<div className="text-text-sub-title-invert text-sm">
|
||||
{t('knowledgeDetails.datasetDescription')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size={'sm'}>
|
||||
<Upload />
|
||||
{t('knowledgeDetails.addFile')}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuItem onClick={showDocumentUploadModal}>
|
||||
{t('fileManager.uploadFile')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={showCreateModal}>
|
||||
{t('knowledgeDetails.emptyFiles')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ListFilterBar>
|
||||
{rowSelectionIsEmpty || (
|
||||
<BulkOperateBar list={list} count={selectedCount}></BulkOperateBar>
|
||||
)}
|
||||
<DatasetTable
|
||||
documents={documents}
|
||||
pagination={pagination}
|
||||
setPagination={setPagination}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
loading={loading}
|
||||
></DatasetTable>
|
||||
{documentUploadVisible && (
|
||||
<FileUploadDialog
|
||||
hideModal={hideDocumentUploadModal}
|
||||
onOk={onDocumentUploadOk}
|
||||
loading={documentUploadLoading}
|
||||
showParseOnCreation
|
||||
></FileUploadDialog>
|
||||
)}
|
||||
{createVisible && (
|
||||
<RenameDialog
|
||||
hideModal={hideCreateModal}
|
||||
onOk={onCreateOk}
|
||||
loading={createLoading}
|
||||
title={'File Name'}
|
||||
></RenameDialog>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
95
ragflow_web/src/pages/dataset/dataset/parsing-card.tsx
Normal file
95
ragflow_web/src/pages/dataset/dataset/parsing-card.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
import { RunningStatus, RunningStatusMap } from './constant';
|
||||
|
||||
interface IProps {
|
||||
record: IDocumentInfo;
|
||||
handleShowLog?: (record: IDocumentInfo) => void;
|
||||
}
|
||||
|
||||
function Dot({ run }: { run: RunningStatus }) {
|
||||
const runningStatus = RunningStatusMap[run];
|
||||
return (
|
||||
<span
|
||||
className={'size-1 inline-block rounded'}
|
||||
style={{ backgroundColor: runningStatus.color }}
|
||||
></span>
|
||||
);
|
||||
}
|
||||
|
||||
export const PopoverContent = ({ record }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const label = t(`knowledgeDetails.runningStatus${record.run}`);
|
||||
|
||||
const replaceText = (text: string) => {
|
||||
// Remove duplicate \n
|
||||
const nextText = text.replace(/(\n)\1+/g, '$1');
|
||||
|
||||
const replacedText = reactStringReplace(
|
||||
nextText,
|
||||
/(\[ERROR\].+\s)/g,
|
||||
(match, i) => {
|
||||
return (
|
||||
<span key={i} className={'text-red-600'}>
|
||||
{match}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return replacedText;
|
||||
};
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'process_begin_at',
|
||||
label: t('knowledgeDetails.processBeginAt'),
|
||||
children: record.process_begin_at,
|
||||
},
|
||||
{
|
||||
key: 'knowledgeDetails.process_duration',
|
||||
label: t('processDuration'),
|
||||
children: `${record.process_duration.toFixed(2)} s`,
|
||||
},
|
||||
{
|
||||
key: 'progress_msg',
|
||||
label: t('knowledgeDetails.progressMsg'),
|
||||
children: replaceText(record.progress_msg.trim()),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="flex gap-2 items-center pb-2">
|
||||
<Dot run={record.run}></Dot> {label}
|
||||
</div>
|
||||
<div className="flex flex-col max-h-[50vh] overflow-auto">
|
||||
{items.map((x, idx) => {
|
||||
return (
|
||||
<div key={x.key} className={idx < 2 ? 'flex gap-2' : ''}>
|
||||
<b>{x.label}:</b>
|
||||
<div className={'w-full whitespace-pre-line text-wrap '}>
|
||||
{x.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export function ParsingCard({ record, handleShowLog }: IProps) {
|
||||
return (
|
||||
<Button
|
||||
variant={'transparent'}
|
||||
className="border-none"
|
||||
size={'sm'}
|
||||
onClick={() => handleShowLog?.(record)}
|
||||
>
|
||||
<Dot run={record.run}></Dot>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
180
ragflow_web/src/pages/dataset/dataset/parsing-status-cell.tsx
Normal file
180
ragflow_web/src/pages/dataset/dataset/parsing-status-cell.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import { IconFontFill } from '@/components/icon-font';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { CircleX } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DocumentType, RunningStatus } from './constant';
|
||||
import { ParsingCard } from './parsing-card';
|
||||
import { UseChangeDocumentParserShowType } from './use-change-document-parser';
|
||||
import { useHandleRunDocumentByIds } from './use-run-document';
|
||||
import { UseSaveMetaShowType } from './use-save-meta';
|
||||
import { isParserRunning } from './utils';
|
||||
const IconMap = {
|
||||
[RunningStatus.UNSTART]: (
|
||||
<IconFontFill name="play" className="text-accent-primary" />
|
||||
),
|
||||
[RunningStatus.RUNNING]: (
|
||||
<CircleX size={14} color="rgba(var(--state-error))" />
|
||||
),
|
||||
[RunningStatus.CANCEL]: (
|
||||
<IconFontFill name="reparse" className="text-accent-primary" />
|
||||
),
|
||||
[RunningStatus.DONE]: (
|
||||
<IconFontFill name="reparse" className="text-accent-primary" />
|
||||
),
|
||||
[RunningStatus.FAIL]: (
|
||||
<IconFontFill name="reparse" className="text-accent-primary" />
|
||||
),
|
||||
};
|
||||
|
||||
export function ParsingStatusCell({
|
||||
record,
|
||||
showChangeParserModal,
|
||||
showSetMetaModal,
|
||||
showLog,
|
||||
}: {
|
||||
record: IDocumentInfo;
|
||||
showLog: (record: IDocumentInfo) => void;
|
||||
} & UseChangeDocumentParserShowType &
|
||||
UseSaveMetaShowType) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
run,
|
||||
parser_id,
|
||||
pipeline_id,
|
||||
pipeline_name,
|
||||
progress,
|
||||
chunk_num,
|
||||
id,
|
||||
} = record;
|
||||
const operationIcon = IconMap[run];
|
||||
const p = Number((progress * 100).toFixed(2));
|
||||
const { handleRunDocumentByIds } = useHandleRunDocumentByIds(id);
|
||||
const isRunning = isParserRunning(run);
|
||||
const isZeroChunk = chunk_num === 0;
|
||||
|
||||
const handleOperationIconClick =
|
||||
(shouldDelete: boolean = false) =>
|
||||
() => {
|
||||
handleRunDocumentByIds(record.id, isRunning, shouldDelete);
|
||||
};
|
||||
|
||||
const handleShowChangeParserModal = useCallback(() => {
|
||||
showChangeParserModal(record);
|
||||
}, [record, showChangeParserModal]);
|
||||
|
||||
const handleShowSetMetaModal = useCallback(() => {
|
||||
showSetMetaModal(record);
|
||||
}, [record, showSetMetaModal]);
|
||||
|
||||
const showParse = useMemo(() => {
|
||||
return record.type !== DocumentType.Virtual;
|
||||
}, [record]);
|
||||
|
||||
const handleShowLog = (record: IDocumentInfo) => {
|
||||
showLog(record);
|
||||
};
|
||||
return (
|
||||
<section className="flex gap-8 items-center">
|
||||
<div className="text-ellipsis w-[100px] flex items-center justify-between">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="border-none truncate max-w-32 cursor-pointer px-2 py-1 rounded-sm hover:bg-bg-card">
|
||||
{pipeline_id
|
||||
? pipeline_name || pipeline_id
|
||||
: parser_id === 'naive'
|
||||
? 'general'
|
||||
: parser_id}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{pipeline_id
|
||||
? pipeline_name || pipeline_id
|
||||
: parser_id === 'naive'
|
||||
? 'general'
|
||||
: parser_id}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={handleShowChangeParserModal}>
|
||||
{t('knowledgeDetails.dataPipeline')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleShowSetMetaModal}>
|
||||
{t('knowledgeDetails.setMetaData')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{showParse && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Separator orientation="vertical" className="h-2.5" />
|
||||
{!isParserRunning(run) && (
|
||||
<ConfirmDeleteDialog
|
||||
title={t(`knowledgeDetails.redo`, { chunkNum: chunk_num })}
|
||||
hidden={isZeroChunk || isRunning}
|
||||
onOk={handleOperationIconClick(true)}
|
||||
onCancel={handleOperationIconClick(false)}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer flex items-center gap-3"
|
||||
onClick={
|
||||
isZeroChunk || isRunning
|
||||
? handleOperationIconClick(false)
|
||||
: () => {}
|
||||
}
|
||||
>
|
||||
{!isParserRunning(run) && operationIcon}
|
||||
</div>
|
||||
</ConfirmDeleteDialog>
|
||||
)}
|
||||
{isParserRunning(run) ? (
|
||||
<>
|
||||
<div
|
||||
className="flex items-center gap-1 cursor-pointer"
|
||||
onClick={() => handleShowLog(record)}
|
||||
>
|
||||
<Progress value={p} className="h-1 flex-1 min-w-10" />
|
||||
{p}%
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer flex items-center gap-3"
|
||||
onClick={
|
||||
isZeroChunk || isRunning
|
||||
? handleOperationIconClick(false)
|
||||
: () => {}
|
||||
}
|
||||
>
|
||||
{operationIcon}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<ParsingCard
|
||||
record={record}
|
||||
handleShowLog={handleShowLog}
|
||||
></ParsingCard>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
128
ragflow_web/src/pages/dataset/dataset/set-meta-dialog.tsx
Normal file
128
ragflow_web/src/pages/dataset/dataset/set-meta-dialog.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { TagRenameId } from '@/pages/add-knowledge/constant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ButtonLoading } from '@/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import Editor, { loader } from '@monaco-editor/react';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
loader.config({ paths: { vs: '/vs' } });
|
||||
|
||||
export function SetMetaDialog({
|
||||
hideModal,
|
||||
onOk,
|
||||
loading,
|
||||
initialMetaData,
|
||||
}: IModalProps<any> & { initialMetaData?: IDocumentInfo['meta_fields'] }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const FormSchema = z.object({
|
||||
meta: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: t('knowledgeDetails.pleaseInputJson'),
|
||||
})
|
||||
.trim()
|
||||
.refine(
|
||||
(value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ message: t('knowledgeDetails.pleaseInputJson') },
|
||||
),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {},
|
||||
});
|
||||
|
||||
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
const ret = await onOk?.(data.meta);
|
||||
if (ret) {
|
||||
hideModal?.();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue('meta', JSON.stringify(initialMetaData, null, 4));
|
||||
}, [form, initialMetaData]);
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('knowledgeDetails.setMetaData')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
id={TagRenameId}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel
|
||||
tooltip={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(
|
||||
t('knowledgeDetails.documentMetaTips'),
|
||||
),
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
>
|
||||
{t('knowledgeDetails.metaData')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Editor
|
||||
height={200}
|
||||
defaultLanguage="json"
|
||||
theme="vs-dark"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<ButtonLoading type="submit" form={TagRenameId} loading={loading}>
|
||||
{t('common.save')}
|
||||
</ButtonLoading>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
UseRowSelectionType,
|
||||
useSelectedIds,
|
||||
} from '@/hooks/logic-hooks/use-row-selection';
|
||||
import {
|
||||
useRemoveDocument,
|
||||
useRunDocument,
|
||||
useSetDocumentStatus,
|
||||
} from '@/hooks/use-document-request';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { Ban, CircleCheck, CircleX, Play, Trash2 } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { DocumentType, RunningStatus } from './constant';
|
||||
|
||||
export function useBulkOperateDataset({
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
documents,
|
||||
}: Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'> & {
|
||||
documents: IDocumentInfo[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { selectedIds: selectedRowKeys } = useSelectedIds(
|
||||
rowSelection,
|
||||
documents,
|
||||
);
|
||||
|
||||
const { runDocumentByIds } = useRunDocument();
|
||||
const { setDocumentStatus } = useSetDocumentStatus();
|
||||
const { removeDocument } = useRemoveDocument();
|
||||
|
||||
const runDocument = useCallback(
|
||||
(run: number) => {
|
||||
const nonVirtualKeys = selectedRowKeys.filter(
|
||||
(x) =>
|
||||
!documents.some((y) => x === y.id && y.type === DocumentType.Virtual),
|
||||
);
|
||||
|
||||
if (nonVirtualKeys.length === 0) {
|
||||
toast.error(t('Please select a non-empty file list'));
|
||||
return;
|
||||
}
|
||||
runDocumentByIds({
|
||||
documentIds: nonVirtualKeys,
|
||||
run,
|
||||
shouldDelete: false,
|
||||
});
|
||||
},
|
||||
[documents, runDocumentByIds, selectedRowKeys, t],
|
||||
);
|
||||
|
||||
const handleRunClick = useCallback(() => {
|
||||
runDocument(1);
|
||||
}, [runDocument]);
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
runDocument(2);
|
||||
}, [runDocument]);
|
||||
|
||||
const onChangeStatus = useCallback(
|
||||
(enabled: boolean) => {
|
||||
setDocumentStatus({ status: enabled, documentId: selectedRowKeys });
|
||||
},
|
||||
[selectedRowKeys, setDocumentStatus],
|
||||
);
|
||||
|
||||
const handleEnableClick = useCallback(() => {
|
||||
onChangeStatus(true);
|
||||
}, [onChangeStatus]);
|
||||
|
||||
const handleDisableClick = useCallback(() => {
|
||||
onChangeStatus(false);
|
||||
}, [onChangeStatus]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
const deletedKeys = selectedRowKeys.filter(
|
||||
(x) =>
|
||||
!documents
|
||||
.filter((y) => y.run === RunningStatus.RUNNING)
|
||||
.some((y) => y.id === x),
|
||||
);
|
||||
if (deletedKeys.length === 0) {
|
||||
toast.error(t('theDocumentBeingParsedCannotBeDeleted'));
|
||||
return;
|
||||
}
|
||||
|
||||
return removeDocument(deletedKeys);
|
||||
}, [selectedRowKeys, removeDocument, documents, t]);
|
||||
|
||||
const list = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: t('knowledgeDetails.enabled'),
|
||||
icon: <CircleCheck />,
|
||||
onClick: handleEnableClick,
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: t('knowledgeDetails.disabled'),
|
||||
icon: <Ban />,
|
||||
onClick: handleDisableClick,
|
||||
},
|
||||
{
|
||||
id: 'run',
|
||||
label: t('knowledgeDetails.run'),
|
||||
icon: <Play />,
|
||||
onClick: handleRunClick,
|
||||
},
|
||||
{
|
||||
id: 'cancel',
|
||||
label: t('knowledgeDetails.cancel'),
|
||||
icon: <CircleX />,
|
||||
onClick: handleCancelClick,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
label: t('common.delete'),
|
||||
icon: <Trash2 />,
|
||||
onClick: async () => {
|
||||
const code = await handleDelete();
|
||||
if (code === 0) {
|
||||
setRowSelection({});
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return { list };
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useSetDocumentParser } from '@/hooks/use-document-request';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { IChangeParserRequestBody } from '@/interfaces/request/document';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export const useChangeDocumentParser = () => {
|
||||
const { setDocumentParser, loading } = useSetDocumentParser();
|
||||
const [record, setRecord] = useState<IDocumentInfo>({} as IDocumentInfo);
|
||||
|
||||
const {
|
||||
visible: changeParserVisible,
|
||||
hideModal: hideChangeParserModal,
|
||||
showModal: showChangeParserModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onChangeParserOk = useCallback(
|
||||
async (parserConfigInfo: IChangeParserRequestBody) => {
|
||||
if (record?.id) {
|
||||
const ret = await setDocumentParser({
|
||||
parserId: parserConfigInfo.parser_id,
|
||||
pipelineId: parserConfigInfo.pipeline_id,
|
||||
documentId: record?.id,
|
||||
parserConfig: parserConfigInfo.parser_config,
|
||||
});
|
||||
if (ret === 0) {
|
||||
hideChangeParserModal();
|
||||
}
|
||||
}
|
||||
},
|
||||
[record?.id, setDocumentParser, hideChangeParserModal],
|
||||
);
|
||||
|
||||
const handleShowChangeParserModal = useCallback(
|
||||
(row: IDocumentInfo) => {
|
||||
setRecord(row);
|
||||
showChangeParserModal();
|
||||
},
|
||||
[showChangeParserModal],
|
||||
);
|
||||
|
||||
return {
|
||||
changeParserLoading: loading,
|
||||
onChangeParserOk,
|
||||
changeParserVisible,
|
||||
hideChangeParserModal,
|
||||
showChangeParserModal: handleShowChangeParserModal,
|
||||
changeParserRecord: record,
|
||||
};
|
||||
};
|
||||
|
||||
export type UseChangeDocumentParserShowType = Pick<
|
||||
ReturnType<typeof useChangeDocumentParser>,
|
||||
'showChangeParserModal'
|
||||
>;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useCreateDocument } from '@/hooks/use-document-request';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useCreateEmptyDocument = () => {
|
||||
const { createDocument, loading } = useCreateDocument();
|
||||
|
||||
const {
|
||||
visible: createVisible,
|
||||
hideModal: hideCreateModal,
|
||||
showModal: showCreateModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onCreateOk = useCallback(
|
||||
async (name: string) => {
|
||||
const ret = await createDocument(name);
|
||||
if (ret === 0) {
|
||||
hideCreateModal();
|
||||
}
|
||||
},
|
||||
[hideCreateModal, createDocument],
|
||||
);
|
||||
|
||||
return {
|
||||
createLoading: loading,
|
||||
onCreateOk,
|
||||
createVisible,
|
||||
hideCreateModal,
|
||||
showCreateModal,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,178 @@
|
||||
import { FileIcon } from '@/components/icon-font';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useSetDocumentStatus } from '@/hooks/use-document-request';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { ColumnDef } from '@tanstack/table-core';
|
||||
import { ArrowUpDown } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DatasetActionCell } from './dataset-action-cell';
|
||||
import { ParsingStatusCell } from './parsing-status-cell';
|
||||
import { UseChangeDocumentParserShowType } from './use-change-document-parser';
|
||||
import { UseRenameDocumentShowType } from './use-rename-document';
|
||||
import { UseSaveMetaShowType } from './use-save-meta';
|
||||
|
||||
type UseDatasetTableColumnsType = UseChangeDocumentParserShowType &
|
||||
UseRenameDocumentShowType &
|
||||
UseSaveMetaShowType & { showLog: (record: IDocumentInfo) => void };
|
||||
|
||||
export function useDatasetTableColumns({
|
||||
showChangeParserModal,
|
||||
showRenameModal,
|
||||
showSetMetaModal,
|
||||
showLog,
|
||||
}: UseDatasetTableColumnsType) {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'knowledgeDetails',
|
||||
});
|
||||
|
||||
const { navigateToChunkParsedResult } = useNavigatePage();
|
||||
const { setDocumentStatus } = useSetDocumentStatus();
|
||||
|
||||
const columns: ColumnDef<IDocumentInfo>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="border-none"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
{t('name')}
|
||||
<ArrowUpDown />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
meta: { cellClassName: 'max-w-[20vw]' },
|
||||
cell: ({ row }) => {
|
||||
const name: string = row.getValue('name');
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="flex gap-2 cursor-pointer"
|
||||
onClick={navigateToChunkParsedResult(
|
||||
row.original.id,
|
||||
row.original.kb_id,
|
||||
)}
|
||||
>
|
||||
<FileIcon name={name}></FileIcon>
|
||||
<span className={cn('truncate')}>{name}</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{name}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'create_time',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="border-none"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
>
|
||||
{t('uploadDate')}
|
||||
<ArrowUpDown />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className="lowercase">
|
||||
{formatDate(row.getValue('create_time'))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: t('enabled'),
|
||||
cell: ({ row }) => {
|
||||
const id = row.original.id;
|
||||
return (
|
||||
<Switch
|
||||
checked={row.getValue('status') === '1'}
|
||||
onCheckedChange={(e) => {
|
||||
setDocumentStatus({ status: e, documentId: id });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'chunk_num',
|
||||
header: t('chunkNumber'),
|
||||
cell: ({ row }) => (
|
||||
<div className="capitalize">{row.getValue('chunk_num')}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'run',
|
||||
header: t('Parse'),
|
||||
// meta: { cellClassName: 'min-w-[20vw]' },
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<ParsingStatusCell
|
||||
record={row.original}
|
||||
showChangeParserModal={showChangeParserModal}
|
||||
showSetMetaModal={showSetMetaModal}
|
||||
showLog={showLog}
|
||||
></ParsingStatusCell>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: t('action'),
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const record = row.original;
|
||||
|
||||
return (
|
||||
<DatasetActionCell
|
||||
record={record}
|
||||
showRenameModal={showRenameModal}
|
||||
></DatasetActionCell>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return columns;
|
||||
}
|
||||
49
ragflow_web/src/pages/dataset/dataset/use-rename-document.ts
Normal file
49
ragflow_web/src/pages/dataset/dataset/use-rename-document.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useSaveDocumentName } from '@/hooks/use-document-request';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export const useRenameDocument = () => {
|
||||
const { saveName, loading } = useSaveDocumentName();
|
||||
const [record, setRecord] = useState<IDocumentInfo>();
|
||||
|
||||
const {
|
||||
visible: renameVisible,
|
||||
hideModal: hideRenameModal,
|
||||
showModal: showRenameModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onRenameOk = useCallback(
|
||||
async (name: string) => {
|
||||
if (record?.id) {
|
||||
const ret = await saveName({ documentId: record.id, name });
|
||||
if (ret === 0) {
|
||||
hideRenameModal();
|
||||
}
|
||||
}
|
||||
},
|
||||
[record?.id, saveName, hideRenameModal],
|
||||
);
|
||||
|
||||
const handleShow = useCallback(
|
||||
(row: IDocumentInfo) => {
|
||||
setRecord(row);
|
||||
showRenameModal();
|
||||
},
|
||||
[showRenameModal],
|
||||
);
|
||||
|
||||
return {
|
||||
renameLoading: loading,
|
||||
onRenameOk,
|
||||
renameVisible,
|
||||
hideRenameModal,
|
||||
showRenameModal: handleShow,
|
||||
initialName: record?.name,
|
||||
};
|
||||
};
|
||||
|
||||
export type UseRenameDocumentShowType = Pick<
|
||||
ReturnType<typeof useRenameDocument>,
|
||||
'showRenameModal'
|
||||
>;
|
||||
34
ragflow_web/src/pages/dataset/dataset/use-run-document.ts
Normal file
34
ragflow_web/src/pages/dataset/dataset/use-run-document.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useRunDocument } from '@/hooks/use-document-request';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useHandleRunDocumentByIds = (id: string) => {
|
||||
const { runDocumentByIds, loading } = useRunDocument();
|
||||
const [currentId, setCurrentId] = useState<string>('');
|
||||
const isLoading = loading && currentId !== '' && currentId === id;
|
||||
|
||||
const handleRunDocumentByIds = async (
|
||||
documentId: string,
|
||||
isRunning: boolean,
|
||||
shouldDelete: boolean = false,
|
||||
) => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
setCurrentId(documentId);
|
||||
try {
|
||||
await runDocumentByIds({
|
||||
documentIds: [documentId],
|
||||
run: isRunning ? 2 : 1,
|
||||
shouldDelete,
|
||||
});
|
||||
setCurrentId('');
|
||||
} catch (error) {
|
||||
setCurrentId('');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleRunDocumentByIds,
|
||||
loading: isLoading,
|
||||
};
|
||||
};
|
||||
50
ragflow_web/src/pages/dataset/dataset/use-save-meta.ts
Normal file
50
ragflow_web/src/pages/dataset/dataset/use-save-meta.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useSetDocumentMeta } from '@/hooks/use-document-request';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export const useSaveMeta = () => {
|
||||
const { setDocumentMeta, loading } = useSetDocumentMeta();
|
||||
const [record, setRecord] = useState<IDocumentInfo>({} as IDocumentInfo);
|
||||
|
||||
const {
|
||||
visible: setMetaVisible,
|
||||
hideModal: hideSetMetaModal,
|
||||
showModal: showSetMetaModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const onSetMetaModalOk = useCallback(
|
||||
async (meta: string) => {
|
||||
const ret = await setDocumentMeta({
|
||||
documentId: record?.id,
|
||||
meta,
|
||||
});
|
||||
if (ret === 0) {
|
||||
hideSetMetaModal();
|
||||
}
|
||||
},
|
||||
[setDocumentMeta, record?.id, hideSetMetaModal],
|
||||
);
|
||||
|
||||
const handleShowSetMetaModal = useCallback(
|
||||
(row: IDocumentInfo) => {
|
||||
setRecord(row);
|
||||
showSetMetaModal();
|
||||
},
|
||||
[showSetMetaModal],
|
||||
);
|
||||
|
||||
return {
|
||||
setMetaLoading: loading,
|
||||
onSetMetaModalOk,
|
||||
setMetaVisible,
|
||||
hideSetMetaModal,
|
||||
showSetMetaModal: handleShowSetMetaModal,
|
||||
metaRecord: record,
|
||||
};
|
||||
};
|
||||
|
||||
export type UseSaveMetaShowType = Pick<
|
||||
ReturnType<typeof useSaveMeta>,
|
||||
'showSetMetaModal'
|
||||
>;
|
||||
36
ragflow_web/src/pages/dataset/dataset/use-select-filters.ts
Normal file
36
ragflow_web/src/pages/dataset/dataset/use-select-filters.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { FilterCollection } from '@/components/list-filter-bar/interface';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useGetDocumentFilter } from '@/hooks/use-document-request';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useSelectDatasetFilters() {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const { filter, onOpenChange } = useGetDocumentFilter();
|
||||
|
||||
const fileTypes = useMemo(() => {
|
||||
if (filter.suffix) {
|
||||
return Object.keys(filter.suffix).map((x) => ({
|
||||
id: x,
|
||||
label: x.toUpperCase(),
|
||||
count: filter.suffix[x],
|
||||
}));
|
||||
}
|
||||
}, [filter.suffix]);
|
||||
const fileStatus = useMemo(() => {
|
||||
if (filter.run_status) {
|
||||
return Object.keys(filter.run_status).map((x) => ({
|
||||
id: x,
|
||||
label: t(`runningStatus${x}`),
|
||||
count: filter.run_status[x as unknown as number],
|
||||
}));
|
||||
}
|
||||
}, [filter.run_status, t]);
|
||||
const filters: FilterCollection[] = useMemo(() => {
|
||||
return [
|
||||
{ field: 'type', label: 'File Type', list: fileTypes },
|
||||
{ field: 'run', label: 'Status', list: fileStatus },
|
||||
] as FilterCollection[];
|
||||
}, [fileStatus, fileTypes]);
|
||||
|
||||
return { filters, onOpenChange };
|
||||
}
|
||||
58
ragflow_web/src/pages/dataset/dataset/use-upload-document.ts
Normal file
58
ragflow_web/src/pages/dataset/dataset/use-upload-document.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { UploadFormSchemaType } from '@/components/file-upload-dialog';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import {
|
||||
useRunDocument,
|
||||
useUploadNextDocument,
|
||||
} from '@/hooks/use-document-request';
|
||||
import { getUnSupportedFilesCount } from '@/utils/document-util';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useHandleUploadDocument = () => {
|
||||
const {
|
||||
visible: documentUploadVisible,
|
||||
hideModal: hideDocumentUploadModal,
|
||||
showModal: showDocumentUploadModal,
|
||||
} = useSetModalState();
|
||||
const { uploadDocument, loading } = useUploadNextDocument();
|
||||
const { runDocumentByIds } = useRunDocument();
|
||||
|
||||
const onDocumentUploadOk = useCallback(
|
||||
async ({ fileList, parseOnCreation }: UploadFormSchemaType) => {
|
||||
if (fileList.length > 0) {
|
||||
const ret = await uploadDocument(fileList);
|
||||
if (typeof ret?.message !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ret.code === 0 && parseOnCreation) {
|
||||
runDocumentByIds({
|
||||
documentIds: ret.data.map((x) => x.id),
|
||||
run: 1,
|
||||
shouldDelete: false,
|
||||
});
|
||||
}
|
||||
|
||||
const count = getUnSupportedFilesCount(ret?.message);
|
||||
/// 500 error code indicates that some file types are not supported
|
||||
let code = ret?.code;
|
||||
if (
|
||||
ret?.code === 0 ||
|
||||
(ret?.code === 500 && count !== fileList.length) // Some files were not uploaded successfully, but some were uploaded successfully.
|
||||
) {
|
||||
code = 0;
|
||||
hideDocumentUploadModal();
|
||||
}
|
||||
return code;
|
||||
}
|
||||
},
|
||||
[uploadDocument, runDocumentByIds, hideDocumentUploadModal],
|
||||
);
|
||||
|
||||
return {
|
||||
documentUploadLoading: loading,
|
||||
onDocumentUploadOk,
|
||||
documentUploadVisible,
|
||||
hideDocumentUploadModal,
|
||||
showDocumentUploadModal,
|
||||
};
|
||||
};
|
||||
6
ragflow_web/src/pages/dataset/dataset/utils.ts
Normal file
6
ragflow_web/src/pages/dataset/dataset/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { RunningStatus } from './constant';
|
||||
|
||||
export const isParserRunning = (text: RunningStatus) => {
|
||||
const isRunning = text === RunningStatus.RUNNING;
|
||||
return isRunning;
|
||||
};
|
||||
48
ragflow_web/src/pages/dataset/index.tsx
Normal file
48
ragflow_web/src/pages/dataset/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '@/components/ui/breadcrumb';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'umi';
|
||||
import { SideBar } from './sidebar';
|
||||
|
||||
export default function DatasetWrapper() {
|
||||
const { navigateToDatasetList } = useNavigatePage();
|
||||
const { t } = useTranslation();
|
||||
const { data } = useFetchKnowledgeBaseConfiguration();
|
||||
|
||||
return (
|
||||
<section className="flex h-full flex-col w-full">
|
||||
<PageHeader>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink onClick={navigateToDatasetList}>
|
||||
{t('knowledgeDetails.dataset')}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="w-28 whitespace-nowrap text-ellipsis overflow-hidden">
|
||||
{data.name}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</PageHeader>
|
||||
<div className="flex flex-1 min-h-0">
|
||||
<SideBar></SideBar>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
241
ragflow_web/src/pages/dataset/knowledge-graph/constant.ts
Normal file
241
ragflow_web/src/pages/dataset/knowledge-graph/constant.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
const nodes = [
|
||||
{
|
||||
type: '"ORGANIZATION"',
|
||||
description:
|
||||
'"厦门象屿是一家公司,其营业收入和市场占有率在2018年至2022年间有所变化。"',
|
||||
source_id: '0',
|
||||
id: '"厦门象屿"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2018年是一个时间点,标志着厦门象屿营业收入和市场占有率的记录开始。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2018"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2019年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2019"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2020年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2020"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2021年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2021"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"2022年是一个时间点,厦门象屿的营业收入和市场占有率在此期间有所变化。"',
|
||||
source_id: '0',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2022"',
|
||||
},
|
||||
{
|
||||
type: '"ORGANIZATION"',
|
||||
description:
|
||||
'"厦门象屿股份有限公司是一家公司,中文简称为厦门象屿,外文名称为Xiamen Xiangyu Co.,Ltd.,外文名称缩写为Xiangyu,法定代表人为邓启东。"',
|
||||
source_id: '1',
|
||||
id: '"厦门象屿股份有限公司"',
|
||||
},
|
||||
{
|
||||
type: '"PERSON"',
|
||||
description: '"邓启东是厦门象屿股份有限公司的法定代表人。"',
|
||||
source_id: '1',
|
||||
entity_type: '"PERSON"',
|
||||
id: '"邓启东"',
|
||||
},
|
||||
{
|
||||
type: '"GEO"',
|
||||
description: '"厦门是一个地理位置,与厦门象屿股份有限公司相关。"',
|
||||
source_id: '1',
|
||||
entity_type: '"GEO"',
|
||||
id: '"厦门"',
|
||||
},
|
||||
{
|
||||
type: '"PERSON"',
|
||||
description:
|
||||
'"廖杰 is the Board Secretary, responsible for handling board-related matters and communications."',
|
||||
source_id: '2',
|
||||
id: '"廖杰"',
|
||||
},
|
||||
{
|
||||
type: '"PERSON"',
|
||||
description:
|
||||
'"史经洋 is the Securities Affairs Representative, responsible for handling securities-related matters and communications."',
|
||||
source_id: '2',
|
||||
entity_type: '"PERSON"',
|
||||
id: '"史经洋"',
|
||||
},
|
||||
{
|
||||
type: '"GEO"',
|
||||
description:
|
||||
'"A geographic location in Xiamen, specifically in the Free Trade Zone, where the company\'s office is situated."',
|
||||
source_id: '2',
|
||||
entity_type: '"GEO"',
|
||||
id: '"厦门市湖里区自由贸易试验区厦门片区"',
|
||||
},
|
||||
{
|
||||
type: '"GEO"',
|
||||
description:
|
||||
'"The building where the company\'s office is located, situated at Xiangyu Road, Xiamen."',
|
||||
source_id: '2',
|
||||
entity_type: '"GEO"',
|
||||
id: '"象屿集团大厦"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"Refers to the year 2021, used for comparing financial metrics with the year 2022."',
|
||||
source_id: '3',
|
||||
id: '"2021年"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"Refers to the year 2022, used for presenting current financial metrics and comparing them with the year 2021."',
|
||||
source_id: '3',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"2022年"',
|
||||
},
|
||||
{
|
||||
type: '"EVENT"',
|
||||
description:
|
||||
'"Indicates the focus on key financial metrics in the table, such as weighted averages and percentages."',
|
||||
source_id: '3',
|
||||
entity_type: '"EVENT"',
|
||||
id: '"主要财务指标"',
|
||||
},
|
||||
].map(({ type, ...x }) => ({ ...x }));
|
||||
|
||||
const edges = [
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2018年的营业收入和市场占有率被记录。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2018"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2019年的营业收入和市场占有率有所变化。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2019"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2020年的营业收入和市场占有率有所变化。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2020"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2021年的营业收入和市场占有率有所变化。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2021"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿在2022年的营业收入和市场占有率有所变化。"',
|
||||
source_id: '0',
|
||||
source: '"厦门象屿"',
|
||||
target: '"2022"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿股份有限公司的法定代表人是邓启东。"',
|
||||
source_id: '1',
|
||||
source: '"厦门象屿股份有限公司"',
|
||||
target: '"邓启东"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description: '"厦门象屿股份有限公司位于厦门。"',
|
||||
source_id: '1',
|
||||
source: '"厦门象屿股份有限公司"',
|
||||
target: '"厦门"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"廖杰\'s office is located in the Xiangyu Group Building, indicating his workplace."',
|
||||
source_id: '2',
|
||||
source: '"廖杰"',
|
||||
target: '"象屿集团大厦"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"廖杰 works in the Xiamen Free Trade Zone, a specific area within Xiamen."',
|
||||
source_id: '2',
|
||||
source: '"廖杰"',
|
||||
target: '"厦门市湖里区自由贸易试验区厦门片区"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"史经洋\'s office is also located in the Xiangyu Group Building, indicating his workplace."',
|
||||
source_id: '2',
|
||||
source: '"史经洋"',
|
||||
target: '"象屿集团大厦"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"史经洋 works in the Xiamen Free Trade Zone, a specific area within Xiamen."',
|
||||
source_id: '2',
|
||||
source: '"史经洋"',
|
||||
target: '"厦门市湖里区自由贸易试验区厦门片区"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"The years 2021 and 2022 are related as they are used for comparing financial metrics, showing changes and adjustments over time."',
|
||||
source_id: '3',
|
||||
source: '"2021年"',
|
||||
target: '"2022年"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"The \'主要财务指标\' is related to the year 2021 as it provides the basis for financial comparisons and adjustments."',
|
||||
source_id: '3',
|
||||
source: '"2021年"',
|
||||
target: '"主要财务指标"',
|
||||
},
|
||||
{
|
||||
weight: 2.0,
|
||||
description:
|
||||
'"The \'主要财务指标\' is related to the year 2022 as it presents the current financial metrics and their changes compared to 2021."',
|
||||
source_id: '3',
|
||||
source: '"2022年"',
|
||||
target: '"主要财务指标"',
|
||||
},
|
||||
];
|
||||
|
||||
export const graphData = {
|
||||
directed: false,
|
||||
multigraph: false,
|
||||
graph: {},
|
||||
nodes,
|
||||
edges,
|
||||
combos: [],
|
||||
};
|
||||
141
ragflow_web/src/pages/dataset/knowledge-graph/force-graph.tsx
Normal file
141
ragflow_web/src/pages/dataset/knowledge-graph/force-graph.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { ElementDatum, Graph, IElementEvent } from '@antv/g6';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { buildNodesAndCombos } from './util';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const TooltipColorMap = {
|
||||
combo: 'red',
|
||||
node: 'black',
|
||||
edge: 'blue',
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
data: any;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const ForceGraph = ({ data, show }: IProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const graphRef = useRef<Graph | null>(null);
|
||||
|
||||
const nextData = useMemo(() => {
|
||||
if (!isEmpty(data)) {
|
||||
const graphData = data;
|
||||
const mi = buildNodesAndCombos(graphData.nodes);
|
||||
return { edges: graphData.edges, ...mi };
|
||||
}
|
||||
return { nodes: [], edges: [] };
|
||||
}, [data]);
|
||||
|
||||
const render = useCallback(() => {
|
||||
const graph = new Graph({
|
||||
container: containerRef.current!,
|
||||
autoFit: 'view',
|
||||
autoResize: true,
|
||||
behaviors: [
|
||||
'drag-element',
|
||||
'drag-canvas',
|
||||
'zoom-canvas',
|
||||
'collapse-expand',
|
||||
{
|
||||
type: 'hover-activate',
|
||||
degree: 1, // 👈🏻 Activate relations.
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
type: 'tooltip',
|
||||
enterable: true,
|
||||
getContent: (e: IElementEvent, items: ElementDatum) => {
|
||||
if (Array.isArray(items)) {
|
||||
if (items.some((x) => x?.isCombo)) {
|
||||
return `<p style="font-weight:600;color:red">${items?.[0]?.data?.label}</p>`;
|
||||
}
|
||||
let result = ``;
|
||||
items.forEach((item) => {
|
||||
result += `<section style="color:${TooltipColorMap[e['targetType'] as keyof typeof TooltipColorMap]};"><h3>${item?.id}</h3>`;
|
||||
if (item?.entity_type) {
|
||||
result += `<div style="padding-bottom: 6px;"><b>Entity type: </b>${item?.entity_type}</div>`;
|
||||
}
|
||||
if (item?.weight) {
|
||||
result += `<div><b>Weight: </b>${item?.weight}</div>`;
|
||||
}
|
||||
if (item?.description) {
|
||||
result += `<p>${item?.description}</p>`;
|
||||
}
|
||||
});
|
||||
return result + '</section>';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
type: 'combo-combined',
|
||||
preventOverlap: true,
|
||||
comboPadding: 1,
|
||||
spacing: 100,
|
||||
},
|
||||
node: {
|
||||
style: {
|
||||
size: 150,
|
||||
labelText: (d) => d.id,
|
||||
// labelPadding: 30,
|
||||
labelFontSize: 40,
|
||||
// labelOffsetX: 20,
|
||||
labelOffsetY: 20,
|
||||
labelPlacement: 'center',
|
||||
labelWordWrap: true,
|
||||
},
|
||||
palette: {
|
||||
type: 'group',
|
||||
field: (d) => {
|
||||
return d?.entity_type as string;
|
||||
},
|
||||
},
|
||||
},
|
||||
edge: {
|
||||
style: (model) => {
|
||||
const weight: number = Number(model?.weight) || 2;
|
||||
const lineWeight = weight * 4;
|
||||
return {
|
||||
stroke: '#99ADD1',
|
||||
lineWidth: lineWeight > 10 ? 10 : lineWeight,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (graphRef.current) {
|
||||
graphRef.current.destroy();
|
||||
}
|
||||
|
||||
graphRef.current = graph;
|
||||
|
||||
graph.setData(nextData);
|
||||
|
||||
graph.render();
|
||||
}, [nextData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(data)) {
|
||||
render();
|
||||
}
|
||||
}, [data, render]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={styles.forceContainer}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: show ? 'block' : 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForceGraph;
|
||||
5
ragflow_web/src/pages/dataset/knowledge-graph/index.less
Normal file
5
ragflow_web/src/pages/dataset/knowledge-graph/index.less
Normal file
@@ -0,0 +1,5 @@
|
||||
.forceContainer {
|
||||
:global(.tooltip) {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
}
|
||||
31
ragflow_web/src/pages/dataset/knowledge-graph/index.tsx
Normal file
31
ragflow_web/src/pages/dataset/knowledge-graph/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useFetchKnowledgeGraph } from '@/hooks/use-knowledge-request';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ForceGraph from './force-graph';
|
||||
import { useDeleteKnowledgeGraph } from './use-delete-graph';
|
||||
|
||||
const KnowledgeGraph: React.FC = () => {
|
||||
const { data } = useFetchKnowledgeGraph();
|
||||
const { t } = useTranslation();
|
||||
const { handleDeleteKnowledgeGraph } = useDeleteKnowledgeGraph();
|
||||
|
||||
return (
|
||||
<section className={'w-full h-[90dvh] relative p-6'}>
|
||||
<ConfirmDeleteDialog onOk={handleDeleteKnowledgeGraph}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size={'sm'}
|
||||
className="absolute right-0 top-0 z-50"
|
||||
>
|
||||
<Trash2 /> {t('common.delete')}
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
<ForceGraph data={data?.graph} show></ForceGraph>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeGraph;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useRemoveKnowledgeGraph } from '@/hooks/use-knowledge-request';
|
||||
import { useCallback } from 'react';
|
||||
import { useParams } from 'umi';
|
||||
|
||||
export function useDeleteKnowledgeGraph() {
|
||||
const { removeKnowledgeGraph, loading } = useRemoveKnowledgeGraph();
|
||||
const { navigateToDataset } = useNavigatePage();
|
||||
const { id } = useParams();
|
||||
|
||||
const handleDeleteKnowledgeGraph = useCallback(async () => {
|
||||
const ret = await removeKnowledgeGraph();
|
||||
if (ret === 0 && id) {
|
||||
navigateToDataset(id)();
|
||||
}
|
||||
}, [id, navigateToDataset, removeKnowledgeGraph]);
|
||||
|
||||
return { handleDeleteKnowledgeGraph, loading };
|
||||
}
|
||||
94
ragflow_web/src/pages/dataset/knowledge-graph/util.ts
Normal file
94
ragflow_web/src/pages/dataset/knowledge-graph/util.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
class KeyGenerator {
|
||||
idx = 0;
|
||||
chars: string[] = [];
|
||||
constructor() {
|
||||
const chars = Array(26)
|
||||
.fill(1)
|
||||
.map((x, idx) => String.fromCharCode(97 + idx)); // 26 char
|
||||
this.chars = chars;
|
||||
}
|
||||
generateKey() {
|
||||
const key = this.chars[this.idx];
|
||||
this.idx++;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
// Classify nodes based on edge relationships
|
||||
export class Converter {
|
||||
keyGenerator;
|
||||
dict: Record<string, string> = {}; // key is node id, value is combo
|
||||
constructor() {
|
||||
this.keyGenerator = new KeyGenerator();
|
||||
}
|
||||
buildDict(edges: { source: string; target: string }[]) {
|
||||
edges.forEach((x) => {
|
||||
if (this.dict[x.source] && !this.dict[x.target]) {
|
||||
this.dict[x.target] = this.dict[x.source];
|
||||
} else if (!this.dict[x.source] && this.dict[x.target]) {
|
||||
this.dict[x.source] = this.dict[x.target];
|
||||
} else if (!this.dict[x.source] && !this.dict[x.target]) {
|
||||
this.dict[x.source] = this.dict[x.target] =
|
||||
this.keyGenerator.generateKey();
|
||||
}
|
||||
});
|
||||
return this.dict;
|
||||
}
|
||||
buildNodesAndCombos(nodes: any[], edges: any[]) {
|
||||
this.buildDict(edges);
|
||||
const nextNodes = nodes.map((x) => ({ ...x, combo: this.dict[x.id] }));
|
||||
|
||||
const combos = Object.values(this.dict).reduce<any[]>((pre, cur) => {
|
||||
if (pre.every((x) => x.id !== cur)) {
|
||||
pre.push({
|
||||
id: cur,
|
||||
data: {
|
||||
label: `Combo ${cur}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
|
||||
return { nodes: nextNodes, combos };
|
||||
}
|
||||
}
|
||||
|
||||
export const isDataExist = (data: any) => {
|
||||
return (
|
||||
data?.data && typeof data?.data !== 'boolean' && !isEmpty(data?.data?.graph)
|
||||
);
|
||||
};
|
||||
|
||||
const findCombo = (communities: string[]) => {
|
||||
const combo = Array.isArray(communities) ? communities[0] : undefined;
|
||||
return combo;
|
||||
};
|
||||
|
||||
export const buildNodesAndCombos = (nodes: any[]) => {
|
||||
const combos: any[] = [];
|
||||
nodes.forEach((x) => {
|
||||
const combo = findCombo(x?.communities);
|
||||
if (combo && combos.every((y) => y.data.label !== combo)) {
|
||||
combos.push({
|
||||
isCombo: true,
|
||||
id: uuid(),
|
||||
data: {
|
||||
label: combo,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const nextNodes = nodes.map((x) => {
|
||||
return {
|
||||
...x,
|
||||
combo: combos.find((y) => y.data.label === findCombo(x?.communities))?.id,
|
||||
};
|
||||
});
|
||||
|
||||
return { nodes: nextNodes, combos };
|
||||
};
|
||||
155
ragflow_web/src/pages/dataset/process-log-modal.tsx
Normal file
155
ragflow_web/src/pages/dataset/process-log-modal.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import FileStatusBadge from '@/components/file-status-badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Modal } from '@/components/ui/modal/modal';
|
||||
import { RunningStatusMap } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import React, { useMemo } from 'react';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
import { RunningStatus } from './dataset/constant';
|
||||
export interface ILogInfo {
|
||||
fileType?: string;
|
||||
uploadedBy?: string;
|
||||
uploadDate?: string;
|
||||
processBeginAt?: string;
|
||||
chunkNumber?: number;
|
||||
|
||||
taskId?: string;
|
||||
fileName: string;
|
||||
fileSize?: string;
|
||||
source?: string;
|
||||
task?: string;
|
||||
status?: RunningStatus;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
duration?: string;
|
||||
details: string;
|
||||
}
|
||||
|
||||
interface ProcessLogModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
logInfo: ILogInfo;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const InfoItem: React.FC<{
|
||||
label: string;
|
||||
value: string | React.ReactNode;
|
||||
className?: string;
|
||||
}> = ({ label, value, className = '' }) => {
|
||||
return (
|
||||
<div className={`flex flex-col mb-4 ${className}`}>
|
||||
<span className="text-text-secondary text-sm">{label}</span>
|
||||
<span className="text-text-primary mt-1">{value}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const replaceText = (text: string) => {
|
||||
// Remove duplicate \n
|
||||
const nextText = text.replace(/(\n)\1+/g, '$1');
|
||||
|
||||
const replacedText = reactStringReplace(
|
||||
nextText,
|
||||
/(\[ERROR\].+\s)/g,
|
||||
(match, i) => {
|
||||
return (
|
||||
<span key={i} className={'text-red-600'}>
|
||||
{match}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return replacedText;
|
||||
};
|
||||
const ProcessLogModal: React.FC<ProcessLogModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
logInfo: initData,
|
||||
title,
|
||||
}) => {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
const blackKeyList = [''];
|
||||
console.log('logInfo', initData);
|
||||
const logInfo = useMemo(() => {
|
||||
console.log('logInfo', initData);
|
||||
return initData;
|
||||
}, [initData]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title || 'log'}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={onCancel}>{t('close')}</Button>
|
||||
</div>
|
||||
}
|
||||
className="process-log-modal"
|
||||
>
|
||||
<div className=" rounded-lg">
|
||||
<div className="flex flex-wrap ">
|
||||
{Object?.keys(logInfo).map((key) => {
|
||||
if (
|
||||
blackKeyList.includes(key) ||
|
||||
!logInfo[key as keyof typeof logInfo]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (key === 'details') {
|
||||
return (
|
||||
<div className="w-full mt-2" key={key}>
|
||||
<InfoItem
|
||||
label={t(key)}
|
||||
value={
|
||||
<div className="w-full whitespace-pre-line text-wrap bg-bg-card rounded-lg h-fit max-h-[350px] overflow-y-auto scrollbar-auto p-2.5">
|
||||
{replaceText(logInfo.details)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (key === 'status') {
|
||||
return (
|
||||
<div className="flex flex-col w-1/2" key={key}>
|
||||
<span className="text-text-secondary text-sm">
|
||||
{t('status')}
|
||||
</span>
|
||||
<div className="mt-1">
|
||||
<FileStatusBadge
|
||||
status={logInfo.status as RunningStatus}
|
||||
name={RunningStatusMap[logInfo.status as RunningStatus]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="w-1/2" key={key}>
|
||||
<InfoItem
|
||||
label={t(key)}
|
||||
value={logInfo[key as keyof typeof logInfo]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* <InfoItem label="Details" value={logInfo.details} /> */}
|
||||
{/* <div>
|
||||
<div>Details</div>
|
||||
<div>
|
||||
<ul className="space-y-2">
|
||||
<div className={'w-full whitespace-pre-line text-wrap '}>
|
||||
{replaceText(logInfo.details)}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessLogModal;
|
||||
17
ragflow_web/src/pages/dataset/sidebar/hooks.tsx
Normal file
17
ragflow_web/src/pages/dataset/sidebar/hooks.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Routes } from '@/routes';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'umi';
|
||||
|
||||
export const useHandleMenuClick = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
(key: Routes) => () => {
|
||||
navigate(`${Routes.DatasetBase}${key}/${id}`);
|
||||
},
|
||||
[id, navigate],
|
||||
);
|
||||
|
||||
return { handleMenuClick };
|
||||
};
|
||||
111
ragflow_web/src/pages/dataset/sidebar/index.tsx
Normal file
111
ragflow_web/src/pages/dataset/sidebar/index.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { IconFontFill } from '@/components/icon-font';
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useSecondPathName } from '@/hooks/route-hook';
|
||||
import {
|
||||
useFetchKnowledgeBaseConfiguration,
|
||||
useFetchKnowledgeGraph,
|
||||
} from '@/hooks/use-knowledge-request';
|
||||
import { cn, formatBytes } from '@/lib/utils';
|
||||
import { Routes } from '@/routes';
|
||||
import { formatPureDate } from '@/utils/date';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Banknote, FileSearch2, FolderOpen, Logs } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHandleMenuClick } from './hooks';
|
||||
|
||||
type PropType = {
|
||||
refreshCount?: number;
|
||||
};
|
||||
|
||||
export function SideBar({ refreshCount }: PropType) {
|
||||
const pathName = useSecondPathName();
|
||||
const { handleMenuClick } = useHandleMenuClick();
|
||||
// refreshCount: be for avatar img sync update on top left
|
||||
const { data } = useFetchKnowledgeBaseConfiguration({ refreshCount });
|
||||
const { data: routerData } = useFetchKnowledgeGraph();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items = useMemo(() => {
|
||||
const list = [
|
||||
{
|
||||
icon: <FolderOpen className="size-4" />,
|
||||
label: t(`knowledgeDetails.subbarFiles`),
|
||||
key: Routes.DatasetBase,
|
||||
},
|
||||
{
|
||||
icon: <FileSearch2 className="size-4" />,
|
||||
label: t(`knowledgeDetails.testing`),
|
||||
key: Routes.DatasetTesting,
|
||||
},
|
||||
{
|
||||
icon: <Logs className="size-4" />,
|
||||
label: t(`knowledgeDetails.overview`),
|
||||
key: Routes.DataSetOverview,
|
||||
},
|
||||
{
|
||||
icon: <Banknote className="size-4" />,
|
||||
label: t(`knowledgeDetails.configuration`),
|
||||
key: Routes.DataSetSetting,
|
||||
},
|
||||
];
|
||||
if (!isEmpty(routerData?.graph)) {
|
||||
list.push({
|
||||
icon: <IconFontFill name="knowledgegraph" className="size-4" />,
|
||||
label: t(`knowledgeDetails.knowledgeGraph`),
|
||||
key: Routes.KnowledgeGraph,
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}, [t, routerData]);
|
||||
|
||||
return (
|
||||
<aside className="relative p-5 space-y-8">
|
||||
<div className="flex gap-2.5 max-w-[200px] items-center">
|
||||
<RAGFlowAvatar
|
||||
avatar={data.avatar}
|
||||
name={data.name}
|
||||
className="size-16"
|
||||
></RAGFlowAvatar>
|
||||
<div className=" text-text-secondary text-xs space-y-1 overflow-hidden">
|
||||
<h3 className="text-lg font-semibold line-clamp-1 text-text-primary text-ellipsis overflow-hidden">
|
||||
{data.name}
|
||||
</h3>
|
||||
<div className="flex justify-between">
|
||||
<span>
|
||||
{data.doc_num} {t('knowledgeDetails.files')}
|
||||
</span>
|
||||
<span>{formatBytes(data.size)}</span>
|
||||
</div>
|
||||
<div>
|
||||
{t('knowledgeDetails.created')} {formatPureDate(data.create_time)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[200px] flex flex-col gap-5">
|
||||
{items.map((item, itemIdx) => {
|
||||
const active = '/' + pathName === item.key;
|
||||
return (
|
||||
<Button
|
||||
key={itemIdx}
|
||||
variant={active ? 'secondary' : 'ghost'}
|
||||
className={cn(
|
||||
'w-full justify-start gap-2.5 px-3 relative h-10 text-text-sub-title-invert',
|
||||
{
|
||||
'bg-bg-card': active,
|
||||
'text-text-primary': active,
|
||||
},
|
||||
)}
|
||||
onClick={handleMenuClick(item.key)}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
99
ragflow_web/src/pages/dataset/testing/index.tsx
Normal file
99
ragflow_web/src/pages/dataset/testing/index.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { useTestRetrieval } from '@/hooks/use-knowledge-request';
|
||||
import { t } from 'i18next';
|
||||
import { useState } from 'react';
|
||||
import { TopTitle } from '../dataset-title';
|
||||
import TestingForm from './testing-form';
|
||||
import { TestingResult } from './testing-result';
|
||||
|
||||
export default function RetrievalTesting() {
|
||||
const {
|
||||
loading,
|
||||
setValues,
|
||||
refetch,
|
||||
data,
|
||||
onPaginationChange,
|
||||
page,
|
||||
pageSize,
|
||||
handleFilterSubmit,
|
||||
filterValue,
|
||||
} = useTestRetrieval();
|
||||
|
||||
const [count] = useState(1);
|
||||
|
||||
return (
|
||||
<div className="p-5">
|
||||
<section className="flex justify-between items-center">
|
||||
<TopTitle
|
||||
title={t('knowledgeDetails.retrievalTesting')}
|
||||
description={t('knowledgeDetails.testingDescription')}
|
||||
></TopTitle>
|
||||
{/* <Button>Save as Preset</Button> */}
|
||||
</section>
|
||||
{count === 1 ? (
|
||||
<section className="flex divide-x h-full">
|
||||
<div className="p-4 flex-1">
|
||||
<div className="flex justify-between pb-2.5">
|
||||
<span className="text-text-primary font-semibold text-2xl">
|
||||
{t('knowledgeDetails.testSetting')}
|
||||
</span>
|
||||
{/* <Button variant={'outline'} onClick={addCount}>
|
||||
<Plus /> Add New Test
|
||||
</Button> */}
|
||||
</div>
|
||||
<div className="h-[calc(100vh-241px)] overflow-auto scrollbar-thin">
|
||||
<TestingForm
|
||||
loading={loading}
|
||||
setValues={setValues}
|
||||
refetch={refetch}
|
||||
></TestingForm>
|
||||
</div>
|
||||
</div>
|
||||
<TestingResult
|
||||
data={data}
|
||||
page={page}
|
||||
loading={loading}
|
||||
pageSize={pageSize}
|
||||
filterValue={filterValue}
|
||||
handleFilterSubmit={handleFilterSubmit}
|
||||
onPaginationChange={onPaginationChange}
|
||||
></TestingResult>
|
||||
</section>
|
||||
) : (
|
||||
<section className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<TestingForm
|
||||
loading={loading}
|
||||
setValues={setValues}
|
||||
refetch={refetch}
|
||||
></TestingForm>
|
||||
<TestingResult
|
||||
data={data}
|
||||
page={page}
|
||||
loading={loading}
|
||||
pageSize={pageSize}
|
||||
filterValue={filterValue}
|
||||
handleFilterSubmit={handleFilterSubmit}
|
||||
onPaginationChange={onPaginationChange}
|
||||
></TestingResult>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TestingForm
|
||||
loading={loading}
|
||||
setValues={setValues}
|
||||
refetch={refetch}
|
||||
></TestingForm>
|
||||
<TestingResult
|
||||
data={data}
|
||||
page={page}
|
||||
loading={loading}
|
||||
pageSize={pageSize}
|
||||
filterValue={filterValue}
|
||||
handleFilterSubmit={handleFilterSubmit}
|
||||
onPaginationChange={onPaginationChange}
|
||||
></TestingResult>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
122
ragflow_web/src/pages/dataset/testing/testing-form.tsx
Normal file
122
ragflow_web/src/pages/dataset/testing/testing-form.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CrossLanguageFormField } from '@/components/cross-language-form-field';
|
||||
import { FormContainer } from '@/components/form-container';
|
||||
import {
|
||||
initialTopKValue,
|
||||
RerankFormFields,
|
||||
topKSchema,
|
||||
} from '@/components/rerank';
|
||||
import {
|
||||
initialSimilarityThresholdValue,
|
||||
initialVectorSimilarityWeightValue,
|
||||
SimilaritySliderFormField,
|
||||
similarityThresholdSchema,
|
||||
vectorSimilarityWeightSchema,
|
||||
} from '@/components/similarity-slider';
|
||||
import { ButtonLoading } from '@/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item';
|
||||
import { useTestRetrieval } from '@/hooks/use-knowledge-request';
|
||||
import { trim } from 'lodash';
|
||||
import { CirclePlay } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type TestingFormProps = Pick<
|
||||
ReturnType<typeof useTestRetrieval>,
|
||||
'loading' | 'refetch' | 'setValues'
|
||||
>;
|
||||
|
||||
export default function TestingForm({
|
||||
loading,
|
||||
refetch,
|
||||
setValues,
|
||||
}: TestingFormProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
question: z.string().min(1, {
|
||||
message: t('knowledgeDetails.testTextPlaceholder'),
|
||||
}),
|
||||
...similarityThresholdSchema,
|
||||
...vectorSimilarityWeightSchema,
|
||||
...topKSchema,
|
||||
use_kg: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
...initialSimilarityThresholdValue,
|
||||
...initialVectorSimilarityWeightValue,
|
||||
...initialTopKValue,
|
||||
use_kg: false,
|
||||
},
|
||||
});
|
||||
|
||||
const question = form.watch('question');
|
||||
|
||||
const values = useWatch({ control: form.control });
|
||||
|
||||
useEffect(() => {
|
||||
setValues(values as Required<z.infer<typeof formSchema>>);
|
||||
}, [setValues, values]);
|
||||
|
||||
function onSubmit() {
|
||||
refetch();
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormContainer className="p-10">
|
||||
<SimilaritySliderFormField
|
||||
isTooltipShown={true}
|
||||
></SimilaritySliderFormField>
|
||||
<RerankFormFields></RerankFormFields>
|
||||
<UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField>
|
||||
<CrossLanguageFormField
|
||||
name={'cross_languages'}
|
||||
></CrossLanguageFormField>
|
||||
</FormContainer>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="question"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('knowledgeDetails.testText')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field}></Textarea>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<ButtonLoading
|
||||
type="submit"
|
||||
disabled={!!!trim(question)}
|
||||
loading={loading}
|
||||
>
|
||||
{!loading && <CirclePlay />}
|
||||
{t('knowledgeDetails.testingLabel')}
|
||||
</ButtonLoading>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
121
ragflow_web/src/pages/dataset/testing/testing-result.tsx
Normal file
121
ragflow_web/src/pages/dataset/testing/testing-result.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import Empty from '@/components/empty/empty';
|
||||
import { FormContainer } from '@/components/form-container';
|
||||
import { FilterButton } from '@/components/list-filter-bar';
|
||||
import { FilterPopover } from '@/components/list-filter-bar/filter-popover';
|
||||
import { FilterCollection } from '@/components/list-filter-bar/interface';
|
||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useTestRetrieval } from '@/hooks/use-knowledge-request';
|
||||
import { ITestingChunk } from '@/interfaces/database/knowledge';
|
||||
import { t } from 'i18next';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const similarityList: Array<{ field: keyof ITestingChunk; label: string }> = [
|
||||
{ field: 'similarity', label: 'Hybrid Similarity' },
|
||||
{ field: 'term_similarity', label: 'Term Similarity' },
|
||||
{ field: 'vector_similarity', label: 'Vector Similarity' },
|
||||
];
|
||||
|
||||
const ChunkTitle = ({ item }: { item: ITestingChunk }) => {
|
||||
const { t } = useTranslate('knowledgeDetails');
|
||||
return (
|
||||
<div className="flex gap-3 text-xs text-text-sub-title-invert italic">
|
||||
{similarityList.map((x) => (
|
||||
<div key={x.field} className="space-x-1">
|
||||
<span>{((item[x.field] as number) * 100).toFixed(2)}</span>
|
||||
<span>{t(camelCase(x.field))}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type TestingResultProps = Pick<
|
||||
ReturnType<typeof useTestRetrieval>,
|
||||
| 'data'
|
||||
| 'filterValue'
|
||||
| 'handleFilterSubmit'
|
||||
| 'page'
|
||||
| 'pageSize'
|
||||
| 'onPaginationChange'
|
||||
| 'loading'
|
||||
>;
|
||||
|
||||
export function TestingResult({
|
||||
filterValue,
|
||||
handleFilterSubmit,
|
||||
page,
|
||||
pageSize,
|
||||
loading,
|
||||
onPaginationChange,
|
||||
data,
|
||||
}: TestingResultProps) {
|
||||
const filters: FilterCollection[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
field: 'doc_ids',
|
||||
label: 'File',
|
||||
list:
|
||||
data.doc_aggs?.map((x) => ({
|
||||
id: x.doc_id,
|
||||
label: x.doc_name,
|
||||
count: x.count,
|
||||
})) ?? [],
|
||||
},
|
||||
];
|
||||
}, [data.doc_aggs]);
|
||||
|
||||
return (
|
||||
<div className="p-4 flex-1">
|
||||
<div className="flex justify-between pb-2.5">
|
||||
<span className="text-text-primary font-semibold text-2xl">
|
||||
{t('knowledgeDetails.testResults')}
|
||||
</span>
|
||||
<FilterPopover
|
||||
filters={filters}
|
||||
onChange={handleFilterSubmit}
|
||||
value={filterValue}
|
||||
>
|
||||
<FilterButton></FilterButton>
|
||||
</FilterPopover>
|
||||
</div>
|
||||
{data.chunks?.length > 0 && !loading && (
|
||||
<>
|
||||
<section className="flex flex-col gap-5 overflow-auto h-[calc(100vh-241px)] scrollbar-thin mb-5">
|
||||
{data.chunks?.map((x) => (
|
||||
<FormContainer key={x.chunk_id} className="px-5 py-2.5">
|
||||
<ChunkTitle item={x}></ChunkTitle>
|
||||
<p className="!mt-2.5"> {x.content_with_weight}</p>
|
||||
</FormContainer>
|
||||
))}
|
||||
</section>
|
||||
<RAGFlowPagination
|
||||
total={data.total}
|
||||
onChange={onPaginationChange}
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
></RAGFlowPagination>
|
||||
</>
|
||||
)}
|
||||
{!data.chunks?.length && !loading && (
|
||||
<div className="flex justify-center items-center w-full h-[calc(100vh-241px)]">
|
||||
<div>
|
||||
<Empty>
|
||||
{data.isRuned && (
|
||||
<div className="text-text-secondary">
|
||||
{t('knowledgeDetails.noTestResultsForRuned')}
|
||||
</div>
|
||||
)}
|
||||
{!data.isRuned && (
|
||||
<div className="text-text-secondary">
|
||||
{t('knowledgeDetails.noTestResultsForNotRuned')}
|
||||
</div>
|
||||
)}
|
||||
</Empty>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user