Feat: Display document parsing status #3221 (#7241)

### What problem does this PR solve?

Feat: Display document parsing status #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-04-24 11:45:37 +08:00
committed by GitHub
parent 216cd7474b
commit ff442c48b5
14 changed files with 794 additions and 48 deletions

View File

@ -0,0 +1,17 @@
import { RunningStatus } from '@/constants/knowledge';
export const RunningStatusMap = {
[RunningStatus.UNSTART]: {
label: 'UNSTART',
color: 'cyan',
},
[RunningStatus.RUNNING]: {
label: 'Parsing',
color: 'blue',
},
[RunningStatus.CANCEL]: { label: 'CANCEL', color: 'orange' },
[RunningStatus.DONE]: { label: 'SUCCESS', color: 'blue' },
[RunningStatus.FAIL]: { label: 'FAIL', color: 'red' },
};
export * from '@/constants/knowledge';

View File

@ -23,8 +23,8 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useFetchNextDocumentList } from '@/hooks/document-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { useFetchDocumentList } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document';
import { getExtension } from '@/utils/document-util';
import { useMemo } from 'react';
@ -38,7 +38,7 @@ export function DatasetTable() {
pagination,
// handleInputChange,
setPagination,
} = useFetchNextDocumentList();
} = useFetchDocumentList();
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],

View File

@ -2,7 +2,6 @@ import { useSetModalState } from '@/hooks/common-hooks';
import {
useCreateNextDocument,
useNextWebCrawl,
useRunNextDocument,
useSaveNextDocumentName,
useSetNextDocumentParser,
} from '@/hooks/document-hooks';
@ -159,35 +158,3 @@ export const useHandleWebCrawl = () => {
showWebCrawlUploadModal,
};
};
export const useHandleRunDocumentByIds = (id: string) => {
const { runDocumentByIds, loading } = useRunNextDocument();
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,
};
};

View File

@ -19,7 +19,7 @@ export default function Dataset() {
return (
<section className="p-8">
<ListFilterBar title="Files">
<ListFilterBar title="Dataset">
<Button
variant={'tertiary'}
size={'sm'}

View File

@ -0,0 +1,101 @@
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';
import { RunningStatus, RunningStatusMap } from './constant';
interface IProps {
record: IDocumentInfo;
}
function Dot({ run }: { run: RunningStatus }) {
const runningStatus = RunningStatusMap[run];
return (
<span
className={'size-2 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_duation',
label: t('processDuration'),
children: `${record.process_duation.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 }: IProps) {
return (
<HoverCard>
<HoverCardTrigger asChild>
<Button variant={'ghost'} size={'sm'}>
<Dot run={record.run}></Dot>
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-[40vw]">
<PopoverContent record={record}></PopoverContent>
</HoverCardContent>
</HoverCard>
);
}

View File

@ -0,0 +1,62 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Separator } from '@/components/ui/separator';
import { IDocumentInfo } from '@/interfaces/database/document';
import { CircleX, Play, RefreshCw } from 'lucide-react';
import { RunningStatus } from './constant';
import { ParsingCard } from './parsing-card';
import { useHandleRunDocumentByIds } from './use-run-document';
import { isParserRunning } from './utils';
const IconMap = {
[RunningStatus.UNSTART]: <Play />,
[RunningStatus.RUNNING]: <CircleX />,
[RunningStatus.CANCEL]: <RefreshCw />,
[RunningStatus.DONE]: <RefreshCw />,
[RunningStatus.FAIL]: <RefreshCw />,
};
export function ParsingStatusCell({ record }: { record: IDocumentInfo }) {
const { run, parser_id, 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);
};
return (
<section className="flex gap-2 items-center ">
<div>
<Button variant={'ghost'} size={'sm'}>
{parser_id}
</Button>
<Separator orientation="vertical" />
</div>
<ConfirmDeleteDialog
hidden={isZeroChunk}
onOk={handleOperationIconClick(true)}
onCancel={handleOperationIconClick(false)}
>
<Button
variant={'ghost'}
size={'sm'}
onClick={isZeroChunk ? handleOperationIconClick(false) : () => {}}
>
{operationIcon}
</Button>
</ConfirmDeleteDialog>
{isParserRunning(run) ? (
<Progress value={p} className="h-1" />
) : (
<ParsingCard record={record}></ParsingCard>
)}
</section>
);
}

View File

@ -16,6 +16,7 @@ import {
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';
@ -25,6 +26,7 @@ import { ArrowUpDown, MoreHorizontal, Pencil, Wrench } from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useChangeDocumentParser } from './hooks';
import { ParsingStatusCell } from './parsing-status-cell';
type UseDatasetTableColumnsType = Pick<
ReturnType<typeof useChangeDocumentParser>,
@ -57,6 +59,7 @@ export function useDatasetTableColumns({
// }, [setRecord, showSetMetaModal]);
const { navigateToChunkParsedResult } = useNavigatePage();
const { setDocumentStatus } = useSetDocumentStatus();
const columns: ColumnDef<IDocumentInfo>[] = [
{
@ -94,7 +97,7 @@ export function useDatasetTableColumns({
</Button>
);
},
meta: { cellClassName: 'max-w-[20vw]' },
// meta: { cellClassName: 'max-w-[20vw]' },
cell: ({ row }) => {
const name: string = row.getValue('name');
@ -142,20 +145,34 @@ export function useDatasetTableColumns({
),
},
{
accessorKey: 'parser_id',
header: t('chunkMethod'),
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('parser_id')}</div>
<div className="capitalize">{row.getValue('chunk_num')}</div>
),
},
{
accessorKey: 'run',
header: t('parsingStatus'),
cell: ({ row }) => (
<Button variant="destructive" size={'sm'}>
{row.getValue('run')}
</Button>
),
// meta: { cellClassName: 'min-w-[20vw]' },
cell: ({ row }) => {
return <ParsingStatusCell record={row.original}></ParsingStatusCell>;
},
},
{
id: 'actions',
@ -166,7 +183,6 @@ export function useDatasetTableColumns({
return (
<section className="flex gap-4 items-center">
<Switch id="airplane-mode" />
<Button
variant="icon"
size={'icon'}

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

View File

@ -0,0 +1,6 @@
import { RunningStatus } from './constant';
export const isParserRunning = (text: RunningStatus) => {
const isRunning = text === RunningStatus.RUNNING;
return isRunning;
};