Feat: Use data pipeline to visualize the parsing configuration of the knowledge base (#10423)

### What problem does this PR solve?

#9869

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: jinhai <haijin.chn@gmail.com>
Signed-off-by: Jin Hai <haijin.chn@gmail.com>
Co-authored-by: chanx <1243304602@qq.com>
Co-authored-by: balibabu <cike8899@users.noreply.github.com>
Co-authored-by: Lynn <lynn_inf@hotmail.com>
Co-authored-by: 纷繁下的无奈 <zhileihuang@126.com>
Co-authored-by: huangzl <huangzl@shinemo.com>
Co-authored-by: writinwaters <93570324+writinwaters@users.noreply.github.com>
Co-authored-by: Wilmer <33392318@qq.com>
Co-authored-by: Adrian Weidig <adrianweidig@gmx.net>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Yongteng Lei <yongtengrey@outlook.com>
Co-authored-by: Liu An <asiro@qq.com>
Co-authored-by: buua436 <66937541+buua436@users.noreply.github.com>
Co-authored-by: BadwomanCraZY <511528396@qq.com>
Co-authored-by: cucusenok <31804608+cucusenok@users.noreply.github.com>
Co-authored-by: Russell Valentine <russ@coldstonelabs.org>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Billy Bao <newyorkupperbay@gmail.com>
Co-authored-by: Zhedong Cen <cenzhedong2@126.com>
Co-authored-by: TensorNull <129579691+TensorNull@users.noreply.github.com>
Co-authored-by: TensorNull <tensor.null@gmail.com>
Co-authored-by: TeslaZY <TeslaZY@outlook.com>
Co-authored-by: Ajay <160579663+aybanda@users.noreply.github.com>
Co-authored-by: AB <aj@Ajays-MacBook-Air.local>
Co-authored-by: 天海蒼灆 <huangaoqin@tecpie.com>
Co-authored-by: He Wang <wanghechn@qq.com>
Co-authored-by: Atsushi Hatakeyama <atu729@icloud.com>
Co-authored-by: Jin Hai <haijin.chn@gmail.com>
Co-authored-by: Mohamed Mathari <155896313+melmathari@users.noreply.github.com>
Co-authored-by: Mohamed Mathari <nocodeventure@Mac-mini-van-Mohamed.fritz.box>
Co-authored-by: Stephen Hu <stephenhu@seismic.com>
Co-authored-by: Shaun Zhang <zhangwfjh@users.noreply.github.com>
Co-authored-by: zhimeng123 <60221886+zhimeng123@users.noreply.github.com>
Co-authored-by: mxc <mxc@example.com>
Co-authored-by: Dominik Novotný <50611433+SgtMarmite@users.noreply.github.com>
Co-authored-by: EVGENY M <168018528+rjohny55@users.noreply.github.com>
Co-authored-by: mcoder6425 <mcoder64@gmail.com>
Co-authored-by: lemsn <lemsn@msn.com>
Co-authored-by: lemsn <lemsn@126.com>
Co-authored-by: Adrian Gora <47756404+adagora@users.noreply.github.com>
Co-authored-by: Womsxd <45663319+Womsxd@users.noreply.github.com>
Co-authored-by: FatMii <39074672+FatMii@users.noreply.github.com>
This commit is contained in:
Kevin Hu
2025-10-09 12:36:19 +08:00
committed by GitHub
parent ef0aecea3b
commit cbf04ee470
490 changed files with 10630 additions and 30688 deletions

View File

@ -3,7 +3,12 @@ export enum LogTabs {
DATASET_LOGS = 'datasetLogs',
}
export enum processingType {
knowledgeGraph = 'knowledgeGraph',
raptor = 'raptor',
export enum ProcessingType {
knowledgeGraph = 'GraphRAG',
raptor = 'RAPTOR',
}
export const ProcessingTypeMap = {
[ProcessingType.knowledgeGraph]: 'Knowledge Graph',
[ProcessingType.raptor]: 'Raptor',
};

View File

@ -0,0 +1,96 @@
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, 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,
handleFilterSubmit,
};
};
export { useFetchFileLogList, useFetchOverviewTital };

View File

@ -1,13 +1,16 @@
import {
CircleQuestionMark,
Cpu,
FileChartLine,
HardDriveDownload,
} from 'lucide-react';
import { FC, useState } from 'react';
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 { useFetchDocumentList } from '@/hooks/use-document-request';
import { t } from 'i18next';
import { CircleQuestionMark } from 'lucide-react';
import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RunningStatus, RunningStatusMap } from '../dataset/constant';
import { LogTabs } from './dataset-common';
import { DatasetFilter } from './dataset-filter';
import { useFetchFileLogList, useFetchOverviewTital } from './hook';
import FileLogsTable from './overview-table';
interface StatCardProps {
@ -15,15 +18,30 @@ interface StatCardProps {
value: number;
icon: JSX.Element;
children?: JSX.Element;
tooltip?: string;
}
interface CardFooterProcessProps {
success: number;
failed: number;
}
const StatCard: FC<StatCardProps> = ({ title, value, children, icon }) => {
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}
<CircleQuestionMark size={12} />
{tooltip && (
<AntToolTip title={tooltip} trigger="hover">
<CircleQuestionMark size={12} />
</AntToolTip>
)}
</h3>
{icon}
</div>
@ -35,115 +53,228 @@ const StatCard: FC<StatCardProps> = ({ title, value, children, icon }) => {
);
};
interface CardFooterProcessProps {
total: number;
completed: number;
success: number;
failed: number;
}
const CardFooterProcess: FC<CardFooterProcessProps> = ({
total,
completed,
success,
failed,
success = 0,
failed = 0,
}) => {
const { t } = useTranslation();
const successPrecentage = (success / total) * 100;
const failedPrecentage = (failed / total) * 100;
return (
<div className="flex items-center flex-col gap-2">
<div className="flex justify-between w-full text-sm text-text-secondary">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{success}
<span>{t('knowledgeDetails.success')}</span>
<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">
{t('knowledgeDetails.success')}
</div>
</div>
<div className="flex items-center gap-1">
{failed}
<span>{t('knowledgeDetails.failed')}</span>
<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">
{t('knowledgeDetails.failed')}
</div>
</div>
<div>{failed || 0}</div>
</div>
<div className="flex items-center gap-1">
{completed}
<span>{t('knowledgeDetails.completed')}</span>
</div>
</div>
<div className="w-full flex rounded-full h-3 bg-bg-card text-sm font-bold text-text-primary">
<div
className=" rounded-full h-3 bg-accent-primary"
style={{ width: successPrecentage + '%' }}
></div>
<div
className=" rounded-full h-3 bg-state-error"
style={{ width: failedPrecentage + '%' }}
></div>
</div>
</div>
);
};
const filters = [
{
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,
};
}),
},
{
field: 'types',
label: t('knowledgeDetails.task'),
list: [
{
id: 'Parse',
label: 'Parse',
},
{
id: 'Download',
label: 'Download',
},
],
},
];
const FileLogsPage: FC = () => {
const { t } = useTranslation();
const [active, setActive] = useState<(typeof LogTabs)[keyof typeof LogTabs]>(
LogTabs.FILE_LOGS,
);
const mockData = Array(30)
.fill(0)
.map((_, i) => ({
id: i === 0 ? '#952734' : `14`,
fileName: 'PRD for DealBees 1.2 (1).txt',
source: 'GitHub',
pipeline: i === 0 ? 'data demo for...' : i === 1 ? 'test' : 'kikis demo',
startDate: '14/03/2025 14:53:39',
task: i === 0 ? 'Parse' : 'Parser',
status:
i === 0
? 'Success'
: i === 1
? 'Failed'
: i === 2
? 'Running'
: 'Pending',
}));
const pagination = {
current: 1,
pageSize: 30,
total: 100,
};
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,
handleFilterSubmit,
setActive,
} = useFetchFileLogList();
const tableList = useMemo(() => {
console.log('tableList', tableOriginData);
if (tableOriginData && tableOriginData.logs?.length) {
return tableOriginData.logs.map((item) => {
return {
...item,
fileName: item.document_name,
statusName: item.operation_status,
};
});
}
}, [tableOriginData]);
const changeActiveLogs = (active: (typeof LogTabs)[keyof typeof LogTabs]) => {
setActive(active);
};
const handlePaginationChange = (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="Total Files" value={2827} icon={<FileChartLine />}>
<div>+7% from last week</div>
<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="Downloading" value={28} icon={<HardDriveDownload />}>
<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
total={100}
success={8}
failed={2}
completed={15}
success={topAllData.downloads.success}
failed={topAllData.downloads.failed}
/>
</StatCard>
<StatCard title="Processing" value={156} icon={<Cpu />}>
<CardFooterProcess total={20} success={8} failed={2} completed={15} />
<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}
failed={topAllData.processing.failed}
/>
</StatCard>
</div>
{/* Tabs & Search */}
<DatasetFilter active={active} setActive={changeActiveLogs} />
<DatasetFilter
filters={filters as FilterCollection[]}
value={filterValue}
active={active}
setActive={changeActiveLogs}
searchString={searchString}
onSearchChange={handleInputChange}
onChange={handleFilterSubmit}
/>
{/* Table */}
<FileLogsTable
data={mockData}
data={tableList}
pagination={pagination}
setPagination={handlePaginationChange}
pageCount={10}

View 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: IFileLogItem[];
total: number;
}

View File

@ -1,7 +1,6 @@
import FileStatusBadge from '@/components/file-status-badge';
import { FileIcon } from '@/components/icon-font';
import { FileIcon, IconFontFill } from '@/components/icon-font';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import SvgIcon from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import {
@ -12,12 +11,16 @@ import {
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 ProcessLogModal from '@/pages/datasets/process-log-modal';
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,
@ -27,62 +30,43 @@ import {
useReactTable,
} from '@tanstack/react-table';
import { TFunction } from 'i18next';
import { ClipboardList, Eye } from 'lucide-react';
import { Dispatch, FC, SetStateAction, useMemo, useState } from 'react';
import { LogTabs, processingType } from './dataset-common';
interface DocumentLog {
id: string;
fileName: string;
source: string;
pipeline: string;
startDate: string;
task: string;
status: 'Success' | 'Failed' | 'Running' | 'Pending';
}
interface FileLogsTableProps {
data: 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];
}
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>,
setIsModalVisible: Dispatch<SetStateAction<boolean>>,
showLog: (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => void,
kowledgeId: string,
navigateToDataflowResult: (
id: string,
knowledgeId?: string | undefined,
props: NavigateToDataflowResultProps,
) => () => void,
) => {
// const { t } = useTranslate('knowledgeDetails');
const columns: ColumnDef<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"
/>
),
},
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',
@ -96,10 +80,10 @@ export const getFileLogsTableColumns = (
cell: ({ row }) => (
<div
className="flex items-center gap-2 text-text-primary"
onClick={navigateToDataflowResult(
row.original.id,
row.original.kb_id,
)}
// onClick={navigateToDataflowResult(
// row.original.id,
// row.original.kb_id,
// )}
>
<FileIcon name={row.original.fileName}></FileIcon>
{row.original.fileName}
@ -107,68 +91,97 @@ export const getFileLogsTableColumns = (
),
},
{
accessorKey: 'source',
accessorKey: 'source_from',
header: t('source'),
cell: ({ row }) => (
<div className="text-text-primary">{row.original.source}</div>
<div className="text-text-primary">{row.original.source_from}</div>
),
},
{
accessorKey: 'pipeline',
accessorKey: 'pipeline_title',
header: t('dataPipeline'),
cell: ({ row }) => (
<div className="flex items-center gap-2 text-text-primary">
<RAGFlowAvatar
avatar={null}
name={row.original.pipeline}
avatar={row.original.avatar}
name={row.original.pipeline_title}
className="size-4"
/>
{row.original.pipeline}
{row.original.pipeline_title}
</div>
),
},
{
accessorKey: 'startDate',
header: t('startDate'),
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">{row.original.startDate}</div>
<div className="text-text-primary">
{formatDate(row.original.process_begin_at)}
</div>
),
},
{
accessorKey: 'task',
accessorKey: 'task_type',
header: t('task'),
cell: ({ row }) => (
<div className="text-text-primary">{row.original.task}</div>
<div className="text-text-primary">{row.original.task_type}</div>
),
},
{
accessorKey: 'status',
accessorKey: 'operation_status',
header: t('status'),
cell: ({ row }) => <FileStatusBadge status={row.original.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">
<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={() => {
setIsModalVisible(true);
showLog(row, LogTabs.FILE_LOGS);
}}
>
<Eye />
</Button>
<Button
variant="ghost"
size="sm"
className="p-1"
onClick={navigateToDataflowResult(row.original.id)}
>
<ClipboardList />
</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>
),
},
@ -179,29 +192,29 @@ export const getFileLogsTableColumns = (
export const getDatasetLogsTableColumns = (
t: TFunction<'translation', string>,
setIsModalVisible: Dispatch<SetStateAction<boolean>>,
showLog: (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => void,
) => {
// const { t } = useTranslate('knowledgeDetails');
const columns: ColumnDef<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"
/>
),
},
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',
@ -210,43 +223,74 @@ export const getDatasetLogsTableColumns = (
),
},
{
accessorKey: 'startDate',
header: t('startDate'),
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">{row.original.startDate}</div>
),
},
{
accessorKey: 'processingType',
header: t('processingType'),
cell: ({ row }) => (
<div className="flex items-center gap-2 text-text-primary">
{processingType.knowledgeGraph === row.original.processingType && (
<SvgIcon name={`data-flow/knowledgegraph`} width={24}></SvgIcon>
)}
{processingType.raptor === row.original.processingType && (
<SvgIcon name={`data-flow/raptor`} width={24}></SvgIcon>
)}
{row.original.processingType}
<div className="text-text-primary">
{formatDate(row.original.process_begin_at)}
</div>
),
},
{
accessorKey: 'status',
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} />,
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">
<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={() => {
setIsModalVisible(true);
showLog(row, LogTabs.DATASET_LOGS);
}}
>
<Eye />
@ -272,11 +316,35 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
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.statusName,
startDate: formatDate(row.original.process_begin_at),
duration: formatSecondsToHumanReadable(
row.original.process_duration || 0,
),
details: row.original.progress_msg,
};
console.log('logDetail', logDetail);
setLogInfo(logDetail);
setIsModalVisible(true);
};
const columns = useMemo(() => {
console.log('columns', active);
return active === LogTabs.FILE_LOGS
? getFileLogsTableColumns(t, setIsModalVisible, navigateToDataflowResult)
: getDatasetLogsTableColumns(t, setIsModalVisible);
? getFileLogsTableColumns(
t,
showLog,
kowledgeId || '',
navigateToDataflowResult,
)
: getDatasetLogsTableColumns(t, showLog);
}, [active, t]);
const currentPagination = useMemo(
@ -287,8 +355,8 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
[pagination],
);
const table = useReactTable({
data,
const table = useReactTable<IFileLogItem & DocumentLog>({
data: data || [],
columns,
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
@ -308,19 +376,9 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
? Math.ceil(pagination.total / pagination.pageSize)
: 0,
});
const taskInfo = {
taskId: '#9527',
fileName: 'PRD for DealBees 1.2 (1).text',
fileSize: '2.4G',
source: 'Github',
task: 'Parse',
state: 'Running',
startTime: '14/03/2025 14:53:39',
duration: '800',
details: 'PRD for DealBees 1.2 (1).text',
};
return (
<div className="w-full h-[calc(100vh-350px)]">
<div className="w-full h-[calc(100vh-360px)]">
<Table rootClassName="max-h-[calc(100vh-380px)]">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@ -337,7 +395,7 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
))}
</TableHeader>
<TableBody className="relative">
{table.getRowModel().rows.length ? (
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
@ -363,7 +421,7 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
)}
</TableBody>
</Table>
<div className="flex items-center justify-end py-4 absolute bottom-3 right-12">
<div className="flex items-center justify-end absolute bottom-3 right-12">
<div className="space-x-2">
<RAGFlowPagination
{...{ current: pagination.current, pageSize: pagination.pageSize }}
@ -372,11 +430,14 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
/>
</div>
</div>
<ProcessLogModal
visible={isModalVisible}
onCancel={() => setIsModalVisible(false)}
taskInfo={taskInfo}
/>
{isModalVisible && (
<ProcessLogModal
title={active === LogTabs.FILE_LOGS ? t('fileLogs') : t('datasetLog')}
visible={isModalVisible}
onCancel={() => setIsModalVisible(false)}
logInfo={logInfo}
/>
)}
</div>
);
};

View File

@ -1,31 +0,0 @@
import { Button } from '@/components/ui/button';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { NaiveConfiguration } from './naive';
import { SavingButton } from './saving-button';
export function ChunkMethodForm() {
const form = useFormContext();
const { t } = useTranslation();
return (
<section className="h-full flex flex-col">
<div className="overflow-auto flex-1 min-h-0">
<NaiveConfiguration></NaiveConfiguration>
</div>
<div className="text-right pt-4 flex justify-end gap-3">
<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>
</section>
);
}

View File

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

View File

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

View File

@ -9,6 +9,9 @@ export function ConfigurationFormContainer({
return <section className={cn('space-y-4', className)}>{children}</section>;
}
export function MainContainer({ children }: PropsWithChildren) {
return <section className="space-y-5">{children}</section>;
export function MainContainer({
children,
className,
}: PropsWithChildren & { className?: string }) {
return <section className={cn('space-y-5', className)}>{children}</section>;
}

View File

@ -1,3 +1,4 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
FormControl,
FormField,
@ -10,15 +11,18 @@ import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import { ArrowUpRight } from 'lucide-react';
import { useFormContext } from 'react-hook-form';
import {
useHasParsedDocument,
useSelectChunkMethodList,
useSelectEmbeddingModelOptions,
} from '../hooks';
export function ChunkMethodItem() {
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);
@ -29,28 +33,29 @@ export function ChunkMethodItem() {
control={form.control}
name={'parser_id'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="flex items-center">
<FormItem className=" items-center space-y-1">
<div className={line === 1 ? 'flex items-center' : ''}>
<FormLabel
required
tooltip={t('chunkMethodTip')}
className="text-sm text-muted-foreground whitespace-wrap w-1/4"
className={cn('text-sm', {
'w-1/4 whitespace-pre-wrap': line === 1,
})}
>
{t('chunkMethod')}
{t('dataPipeline')}
</FormLabel>
<div className="w-3/4 ">
<div className={line === 1 ? 'w-3/4 ' : 'w-full'}>
<FormControl>
<RAGFlowSelect
{...field}
options={parserList}
placeholder={t('chunkMethodPlaceholder')}
// onChange={handleChunkMethodSelectChange}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<div className={line === 1 ? 'w-1/4' : ''}></div>
<FormMessage />
</div>
</FormItem>
@ -58,49 +63,55 @@ export function ChunkMethodItem() {
/>
);
}
export function EmbeddingModelItem({ line = 1 }: { line?: 1 | 2 }) {
export function EmbeddingModelItem({ line = 1, isEdit = true }: IProps) {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
const embeddingModelOptions = useSelectEmbeddingModelOptions();
const disabled = useHasParsedDocument();
const disabled = useHasParsedDocument(isEdit);
return (
<FormField
control={form.control}
name={'embd_id'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className={cn({ 'flex items-center': line === 1 })}>
<FormLabel
required
tooltip={t('embeddingModelTip')}
className={cn('text-sm whitespace-wrap ', {
'w-1/4': line === 1,
<>
<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,
})}
>
{t('embeddingModel')}
</FormLabel>
<div
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
>
<FormControl>
<RAGFlowSelect
{...field}
options={embeddingModelOptions}
disabled={disabled}
placeholder={t('embeddingModelPlaceholder')}
/>
</FormControl>
<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')}
/>
</FormControl>
</div>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
<div className="flex pt-1">
<div className={line === 1 ? 'w-1/4' : ''}></div>
<FormMessage />
</div>
</FormItem>
)}
/>
</>
);
}
@ -142,155 +153,6 @@ export function ParseTypeItem() {
);
}
export function DataFlowItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<FormField
control={form.control}
name={'data_flow'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<div className="flex gap-2 justify-between ">
<FormLabel
tooltip={t('dataFlowTip')}
className="text-sm text-text-primary whitespace-wrap "
>
{t('dataFlow')}
</FormLabel>
<div className="text-sm flex text-text-primary">
{t('buildItFromScratch')}
<ArrowUpRight size={14} />
</div>
</div>
<div className="text-muted-foreground">
<FormControl>
<RAGFlowSelect
{...field}
placeholder={t('dataFlowPlaceholder')}
options={[{ value: '0', label: t('dataFlowDefault') }]}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
export function DataExtractKnowledgeItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<>
{' '}
<FormField
control={form.control}
name={'extractKnowledgeGraph'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('extractKnowledgeGraphTip')}
className="text-sm whitespace-wrap "
>
{t('extractKnowledgeGraph')}
</FormLabel>
<div className="text-muted-foreground">
<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>
)}
/>{' '}
<FormField
control={form.control}
name={'useRAPTORToEnhanceRetrieval'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('useRAPTORToEnhanceRetrievalTip')}
className="text-sm whitespace-wrap "
>
{t('useRAPTORToEnhanceRetrieval')}
</FormLabel>
<div className="text-muted-foreground">
<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 TeamItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<FormField
control={form.control}
name={'team'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('teamTip')}
className="text-sm whitespace-wrap "
>
<span className="text-destructive mr-1"> *</span>
{t('team')}
</FormLabel>
<div className="text-muted-foreground">
<FormControl>
<RAGFlowSelect
{...field}
placeholder={t('teamPlaceholder')}
options={[{ value: '0', label: t('teamDefault') }]}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
export function EnableAutoGenerateItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();

View File

@ -11,6 +11,9 @@ export const formSchema = z.object({
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({
@ -71,3 +74,18 @@ export const formSchema = z.object({
pagerank: z.number(),
// icon: z.array(z.instanceof(File)),
});
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,
// });

View File

@ -1,4 +1,5 @@
import { AvatarUpload } from '@/components/avatar-upload';
import PageRankFormField from '@/components/page-rank-form-field';
import {
FormControl,
FormField,
@ -9,6 +10,7 @@ import {
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';
@ -17,14 +19,14 @@ export function GeneralForm() {
const { t } = useTranslation();
return (
<section className="space-y-4">
<>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="items-center space-y-0">
<div className="flex">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
<FormLabel className="text-sm whitespace-nowrap w-1/4">
<span className="text-red-600">*</span>
{t('common.name')}
</FormLabel>
@ -45,7 +47,7 @@ export function GeneralForm() {
render={({ field }) => (
<FormItem className="items-center space-y-0">
<div className="flex">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
<FormLabel className="text-sm whitespace-nowrap w-1/4">
{t('setting.avatar')}
</FormLabel>
<FormControl className="w-3/4">
@ -70,7 +72,7 @@ export function GeneralForm() {
return (
<FormItem className="items-center space-y-0">
<div className="flex">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
<FormLabel className="text-sm whitespace-nowrap w-1/4">
{t('flow.description')}
</FormLabel>
<FormControl className="w-3/4">
@ -87,6 +89,9 @@ export function GeneralForm() {
/>
<PermissionFormField></PermissionFormField>
<EmbeddingModelItem></EmbeddingModelItem>
</section>
<PageRankFormField></PageRankFormField>
<TagItems></TagItems>
</>
);
}

View File

@ -25,8 +25,10 @@ export function useSelectEmbeddingModelOptions() {
return allOptions[LlmModelType.Embedding];
}
export function useHasParsedDocument() {
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
export function useHasParsedDocument(isEdit?: boolean) {
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration({
isEdit,
});
return knowledgeDetails.chunk_num > 0;
}
@ -39,6 +41,16 @@ export const useFetchKnowledgeConfigurationOnMount = (
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 }, [
@ -52,7 +64,7 @@ export const useFetchKnowledgeConfigurationOnMount = (
'pagerank',
'avatar',
]),
};
} as z.infer<typeof formSchema>;
form.reset(formValues);
}, [form, knowledgeDetails]);

View File

@ -1,16 +1,29 @@
import { IDataPipelineSelectNode } 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 { DocumentParserType } from '@/constants/knowledge';
import { PermissionRole } from '@/constants/permission';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { TopTitle } from '../dataset-title';
import { ChunkMethodForm } from './chunk-method-form';
import {
GenerateType,
IGenerateLogButtonProps,
} from '../dataset/generate-button/generate';
import LinkDataPipeline, {
IDataPipelineNodeProps,
} from './components/link-data-pipeline';
import { MainContainer } from './configuration-form-container';
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',
@ -46,24 +59,84 @@ export default function DatasetSettings() {
html4excel: false,
topn_tags: 3,
raptor: {
use_raptor: false,
use_raptor: true,
max_token: 256,
threshold: 0.1,
max_cluster: 64,
random_seed: 0,
prompt: t('knowledgeConfiguration.promptText'),
},
graphrag: {
use_graphrag: false,
use_graphrag: true,
entity_types: initialEntityTypes,
method: MethodValue.Light,
},
},
pipeline_id: '',
pagerank: 0,
},
});
useFetchKnowledgeConfigurationOnMount(form);
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.mindmap_task_finish_at,
task_id: knowledgeDetails.mindmap_task_id,
} as IGenerateLogButtonProps);
setRaptorGenerateData({
finish_at: knowledgeDetails.raptor_task_finish_at,
task_id: knowledgeDetails.raptor_task_id,
} as IGenerateLogButtonProps);
}
}, [knowledgeDetails]);
async function onSubmit(data: z.infer<typeof formSchema>) {
console.log('🚀 ~ DatasetSettings ~ data:', data);
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);
}
};
return (
<section className="p-5 h-full flex flex-col">
<TopTitle
@ -76,9 +149,41 @@ export default function DatasetSettings() {
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 flex-1"
>
<div className="w-[768px]">
<GeneralForm></GeneralForm>
<ChunkMethodForm></ChunkMethodForm>
<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 />
<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>

View File

@ -1,33 +0,0 @@
import GraphRagItems from '@/components/parse-configuration/graph-rag-form-fields';
import RaptorFormFields from '@/components/parse-configuration/raptor-form-fields';
import {
ConfigurationFormContainer,
MainContainer,
} from './configuration-form-container';
import { EnableAutoGenerateItem } from './configuration/common-item';
export function NaiveConfiguration() {
return (
<MainContainer>
<GraphRagItems className="border-none p-0"></GraphRagItems>
<ConfigurationFormContainer>
<RaptorFormFields></RaptorFormFields>
</ConfigurationFormContainer>
<EnableAutoGenerateItem />
{/* <ConfigurationFormContainer>
<ChunkMethodItem></ChunkMethodItem>
<LayoutRecognizeFormField></LayoutRecognizeFormField>
<MaxTokenNumberFormField initialValue={512}></MaxTokenNumberFormField>
<DelimiterFormField></DelimiterFormField>
</ConfigurationFormContainer>
<ConfigurationFormContainer>
<PageRankFormField></PageRankFormField>
<AutoKeywordsFormField></AutoKeywordsFormField>
<AutoQuestionsFormField></AutoQuestionsFormField>
<ExcelToHtmlFormField></ExcelToHtmlFormField>
<TagItems></TagItems>
</ConfigurationFormContainer> */}
</MainContainer>
);
}

View File

@ -62,7 +62,7 @@ export function SavingButton() {
if (beValid) {
form.handleSubmit(async (values) => {
console.log('saveKnowledgeConfiguration: ', values);
delete values['avatar'];
// delete values['avatar'];
await saveKnowledgeConfiguration({
kb_id,
...values,

View File

@ -3,15 +3,21 @@ import { RunningStatus } from '@/constants/knowledge';
export const RunningStatusMap = {
[RunningStatus.UNSTART]: {
label: 'UNSTART',
color: 'var(--accent-primary)',
color: 'rgba(var(--accent-primary))',
},
[RunningStatus.RUNNING]: {
label: 'Parsing',
color: 'var(--team-member)',
},
[RunningStatus.CANCEL]: { label: 'CANCEL', color: 'var(--state-warning)' },
[RunningStatus.DONE]: { label: 'SUCCESS', color: 'var(--state-success)' },
[RunningStatus.FAIL]: { label: 'FAIL', color: 'var(--state-error' },
[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';

View File

@ -27,8 +27,11 @@ import {
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';
@ -81,11 +84,13 @@ export function DatasetTable({
onSetMetaModalOk,
metaRecord,
} = useSaveMeta();
const { showLog, logInfo, logVisible, hideLog } = useShowLog(documents);
const columns = useDatasetTableColumns({
showChangeParserModal,
showRenameModal,
showSetMetaModal,
showLog,
});
const currentPagination = useMemo(() => {
@ -180,6 +185,7 @@ export function DatasetTable({
<ChunkMethodDialog
documentId={changeParserRecord.id}
parserId={changeParserRecord.parser_id}
pipelineId={changeParserRecord.pipeline_id}
parserConfig={changeParserRecord.parser_config}
documentExtension={getExtension(changeParserRecord.name)}
onOk={onChangeParserOk}
@ -207,6 +213,14 @@ export function DatasetTable({
initialMetaData={metaRecord.meta_fields}
></SetMetaDialog>
)}
{logVisible && (
<ProcessLogModal
title={t('knowledgeDetails.fileLogs')}
visible={logVisible}
onCancel={() => hideLog()}
logInfo={logInfo}
/>
)}
</div>
);
}

View File

@ -0,0 +1,339 @@
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)]':
status === generateStatus.start ||
status === generateStatus.completed,
'hover:border-border hover: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 && (
<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>
)}
<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>
);
};
const Generate: React.FC = () => {
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>
<Button
variant={'transparent'}
onClick={() => {
handleOpenChange(!open);
}}
>
<WandSparkles className="mr-2" />
{t('knowledgeDetails.generate')}
</Button>
</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>
);
};

View File

@ -0,0 +1,174 @@
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 } 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 useDatasetGenerate = () => {
const queryClient = useQueryClient();
const { id } = useParams();
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);
if (data.code === 0) {
message.success(t('message.operated'));
queryClient.invalidateQueries({
queryKey: [type],
});
}
return data;
},
});
return { runGenerate: mutateAsync, pauseGenerate, data, loading };
};
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 };
};

View File

@ -1,68 +0,0 @@
import SvgIcon from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { t } from 'i18next';
import { lowerFirst, toLower } from 'lodash';
import { WandSparkles } from 'lucide-react';
const MenuItem: React.FC<{ name: 'KnowledgeGraph' | 'Raptor' }> = ({
name,
}) => {
console.log(name, 'pppp');
return (
<div className="flex items-start gap-2 flex-col">
<div className="flex justify-start text-text-primary items-center gap-2">
<SvgIcon name={`data-flow/${toLower(name)}`} width={24}></SvgIcon>
{t(`knowledgeDetails.${lowerFirst(name)}`)}
</div>
<div className="text-text-secondary text-sm">
{t(`knowledgeDetails.generate${name}`)}
</div>
</div>
);
};
const Generate: React.FC = () => {
return (
<div className="generate">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'transparent'}>
<WandSparkles className="mr-2" />
{t('knowledgeDetails.generate')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[380px] p-2 ">
<DropdownMenuItem className="border cursor-pointer p-2 rounded-md hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)]">
<MenuItem name="KnowledgeGraph" />
</DropdownMenuItem>
<DropdownMenuItem
className="border cursor-pointer p-2 rounded-md mt-3 hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)]"
onSelect={(e) => {
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
<MenuItem name="Raptor" />
{/* <div className="flex items-start gap-2 flex-col">
<div className="flex items-center gap-2">
<SvgIcon name={`data-flow/raptor`} width={24}></SvgIcon>
{t('knowledgeDetails.raptor')}
</div>
<div>{t('knowledgeDetails.generateRaptor')}</div>
</div> */}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
export default Generate;

View File

@ -1,8 +1,13 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { useNextWebCrawl } from '@/hooks/document-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { useCallback, useState } from 'react';
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();
@ -58,3 +63,43 @@ export const useHandleWebCrawl = () => {
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 };
};

View File

@ -15,7 +15,7 @@ import { useFetchDocumentList } from '@/hooks/use-document-request';
import { Upload } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { DatasetTable } from './dataset-table';
import Generate from './generate';
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';
@ -75,7 +75,7 @@ export default function Dataset() {
filters={filters}
leftPanel={
<div className="items-start">
<div className="pb-1">{t('knowledgeDetails.dataset')}</div>
<div className="pb-1">{t('knowledgeDetails.subbarFiles')}</div>
<div className="text-text-sub-title-invert text-sm">
{t('knowledgeDetails.datasetDescription')}
</div>

View File

@ -1,9 +1,4 @@
import { Button } from '@/components/ui/button';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { IDocumentInfo } from '@/interfaces/database/document';
import { useTranslation } from 'react-i18next';
import reactStringReplace from 'react-string-replace';
@ -11,6 +6,7 @@ import { RunningStatus, RunningStatusMap } from './constant';
interface IProps {
record: IDocumentInfo;
handleShowLog?: (record: IDocumentInfo) => void;
}
function Dot({ run }: { run: RunningStatus }) {
@ -85,17 +81,15 @@ export const PopoverContent = ({ record }: IProps) => {
);
};
export function ParsingCard({ record }: IProps) {
export function ParsingCard({ record, handleShowLog }: IProps) {
return (
<HoverCard>
<HoverCardTrigger asChild>
<Button variant={'transparent'} className="border-none" size={'sm'}>
<Dot run={record.run}></Dot>
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-[40vw]">
<PopoverContent record={record}></PopoverContent>
</HoverCardContent>
</HoverCard>
<Button
variant={'transparent'}
className="border-none"
size={'sm'}
onClick={() => handleShowLog?.(record)}
>
<Dot run={record.run}></Dot>
</Button>
);
}

View File

@ -1,4 +1,5 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { IconFontFill } from '@/components/icon-font';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@ -6,41 +7,56 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { Progress } from '@/components/ui/progress';
import { Separator } from '@/components/ui/separator';
import { IDocumentInfo } from '@/interfaces/database/document';
import { CircleX, RefreshCw } from 'lucide-react';
import { CircleX } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { DocumentType, RunningStatus } from './constant';
import { ParsingCard, PopoverContent } from './parsing-card';
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]: (
<div className="w-0 h-0 border-l-[10px] border-l-accent-primary border-t-8 border-r-4 border-b-8 border-transparent"></div>
<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" />
),
[RunningStatus.RUNNING]: <CircleX size={14} color="var(--state-error)" />,
[RunningStatus.CANCEL]: <RefreshCw size={14} color="var(--accent-primary)" />,
[RunningStatus.DONE]: <RefreshCw size={14} color="var(--accent-primary)" />,
[RunningStatus.FAIL]: <RefreshCw size={14} color="var(--accent-primary)" />,
};
export function ParsingStatusCell({
record,
showChangeParserModal,
showSetMetaModal,
}: { record: IDocumentInfo } & UseChangeDocumentParserShowType &
showLog,
}: {
record: IDocumentInfo;
showLog: (record: IDocumentInfo) => void;
} & UseChangeDocumentParserShowType &
UseSaveMetaShowType) {
const { t } = useTranslation();
const { run, parser_id, progress, chunk_num, id } = record;
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);
@ -65,18 +81,25 @@ export function ParsingStatusCell({
return record.type !== DocumentType.Virtual;
}, [record]);
const handleShowLog = (record: IDocumentInfo) => {
showLog(record);
};
return (
<section className="flex gap-8 items-center">
<div className="w-[100px] text-ellipsis overflow-hidden flex items-center justify-between">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'transparent'} className="border-none" size={'sm'}>
{parser_id === 'naive' ? 'general' : parser_id}
{pipeline_id
? pipeline_name || pipeline_id
: parser_id === 'naive'
? 'general'
: parser_id}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleShowChangeParserModal}>
{t('knowledgeDetails.chunkMethod')}
{t('knowledgeDetails.dataPipeline')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleShowSetMetaModal}>
{t('knowledgeDetails.setMetaData')}
@ -85,41 +108,54 @@ export function ParsingStatusCell({
</DropdownMenu>
</div>
{showParse && (
<>
<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)
: () => {}
}
<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)}
>
<Separator orientation="vertical" className="h-2.5" />
{operationIcon}
</div>
</ConfirmDeleteDialog>
{isParserRunning(run) ? (
<HoverCard>
<HoverCardTrigger asChild>
<div className="flex items-center gap-1">
<Progress value={p} className="h-1 flex-1 min-w-10" />
{p}%
</div>
</HoverCardTrigger>
<HoverCardContent className="w-[40vw]">
<PopoverContent record={record}></PopoverContent>
</HoverCardContent>
</HoverCard>
) : (
<ParsingCard record={record}></ParsingCard>
<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>
);

View File

@ -19,6 +19,7 @@ export const useChangeDocumentParser = () => {
if (record?.id) {
const ret = await setDocumentParser({
parserId: parserConfigInfo.parser_id,
pipelineId: parserConfigInfo.pipeline_id,
documentId: record?.id,
parserConfig: parserConfigInfo.parser_config,
});

View File

@ -23,12 +23,13 @@ import { UseSaveMetaShowType } from './use-save-meta';
type UseDatasetTableColumnsType = UseChangeDocumentParserShowType &
UseRenameDocumentShowType &
UseSaveMetaShowType;
UseSaveMetaShowType & { showLog: (record: IDocumentInfo) => void };
export function useDatasetTableColumns({
showChangeParserModal,
showRenameModal,
showSetMetaModal,
showLog,
}: UseDatasetTableColumnsType) {
const { t } = useTranslation('translation', {
keyPrefix: 'knowledgeDetails',
@ -151,6 +152,7 @@ export function useDatasetTableColumns({
record={row.original}
showChangeParserModal={showChangeParserModal}
showSetMetaModal={showSetMetaModal}
showLog={showLog}
></ParsingStatusCell>
);
},

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

View File

@ -9,7 +9,7 @@ import { cn, formatBytes } from '@/lib/utils';
import { Routes } from '@/routes';
import { formatPureDate } from '@/utils/date';
import { isEmpty } from 'lodash';
import { Banknote, Database, FileSearch2, GitGraph } from 'lucide-react';
import { Banknote, DatabaseZap, FileSearch2, FolderOpen } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHandleMenuClick } from './hooks';
@ -22,36 +22,36 @@ 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 } = useFetchKnowledgeBaseConfiguration({ refreshCount });
const { data: routerData } = useFetchKnowledgeGraph();
const { t } = useTranslation();
const items = useMemo(() => {
const list = [
// {
// icon: DatabaseZap,
// label: t(`knowledgeDetails.overview`),
// key: Routes.DataSetOverview,
// },
{
icon: Database,
label: t(`knowledgeDetails.dataset`),
icon: <DatabaseZap className="size-4" />,
label: t(`knowledgeDetails.overview`),
key: Routes.DataSetOverview,
},
{
icon: <FolderOpen className="size-4" />,
label: t(`knowledgeDetails.subbarFiles`),
key: Routes.DatasetBase,
},
{
icon: FileSearch2,
icon: <FileSearch2 className="size-4" />,
label: t(`knowledgeDetails.testing`),
key: Routes.DatasetTesting,
},
{
icon: Banknote,
icon: <Banknote className="size-4" />,
label: t(`knowledgeDetails.configuration`),
key: Routes.DatasetSetting,
key: Routes.DataSetSetting,
},
];
if (!isEmpty(routerData?.graph)) {
list.push({
icon: GitGraph,
icon: <IconFontFill name="knowledgegraph" className="size-4" />,
label: t(`knowledgeDetails.knowledgeGraph`),
key: Routes.KnowledgeGraph,
});
@ -99,7 +99,7 @@ export function SideBar({ refreshCount }: PropType) {
)}
onClick={handleMenuClick(item.key)}
>
<item.icon className="size-4" />
{item.icon}
<span>{item.label}</span>
</Button>
);