Fix: Optimized the timeline component and parser editing features #9869 (#10268)

### What problem does this PR solve?

Fix: Optimized the timeline component and parser editing features #9869

- Introduced the TimelineNodeType type, restructured the timeline node
structure, and supported dynamic node generation
- Enhanced the FormatPreserveEditor component to support editing and
line wrapping of JSON-formatted content
- Added a rerun function and loading state to the parser and splitter
components
- Adjusted the timeline style and interaction logic to enhance the user
experience
- Improved the modal component and added a destroy method to support
more flexible control
- Optimized the chunk result display and operation logic, supporting
batch deletion and selection
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2025-09-24 19:58:30 +08:00
committed by GitHub
parent 8be7380b79
commit a6039cf563
21 changed files with 645 additions and 264 deletions

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { TimelineNodeType } from '@/pages/dataflow-result/constant';
import { parseColorToRGB } from '@/utils/common-util'; import { parseColorToRGB } from '@/utils/common-util';
import { Slot } from '@radix-ui/react-slot'; import { Slot } from '@radix-ui/react-slot';
import * as React from 'react'; import * as React from 'react';
@ -220,6 +221,8 @@ interface TimelineNode
completed?: boolean; completed?: boolean;
clickable?: boolean; clickable?: boolean;
activeStyle?: TimelineIndicatorNodeProps; activeStyle?: TimelineIndicatorNodeProps;
detail?: any;
type?: TimelineNodeType;
} }
interface CustomTimelineProps extends React.HTMLAttributes<HTMLDivElement> { interface CustomTimelineProps extends React.HTMLAttributes<HTMLDivElement> {
@ -252,7 +255,6 @@ const CustomTimeline = ({
const [internalActiveStep, setInternalActiveStep] = const [internalActiveStep, setInternalActiveStep] =
React.useState(defaultValue); React.useState(defaultValue);
const _lineColor = `rgb(${parseColorToRGB(lineColor)})`; const _lineColor = `rgb(${parseColorToRGB(lineColor)})`;
console.log(lineColor, _lineColor);
const currentActiveStep = activeStep ?? internalActiveStep; const currentActiveStep = activeStep ?? internalActiveStep;
const handleStepChange = (step: number, id: string | number) => { const handleStepChange = (step: number, id: string | number) => {
@ -284,8 +286,6 @@ const CustomTimeline = ({
typeof _nodeSizeTemp === 'number' typeof _nodeSizeTemp === 'number'
? `${_nodeSizeTemp}px` ? `${_nodeSizeTemp}px`
: _nodeSizeTemp; : _nodeSizeTemp;
console.log('icon-size', nodeSize, node.nodeSize, _nodeSize);
// const activeStyle = _activeStyle || {};
return ( return (
<TimelineItem <TimelineItem
@ -372,11 +372,10 @@ const CustomTimeline = ({
)} )}
</TimelineIndicator> </TimelineIndicator>
<TimelineHeader> <TimelineHeader className="transform -translate-x-[40%] text-center">
{node.date && <TimelineDate>{node.date}</TimelineDate>}
<TimelineTitle <TimelineTitle
className={cn( className={cn(
'text-sm font-medium', 'text-sm font-medium -ml-1',
isActive && _activeStyle.textColor isActive && _activeStyle.textColor
? `text-${_activeStyle.textColor}` ? `text-${_activeStyle.textColor}`
: '', : '',
@ -387,6 +386,7 @@ const CustomTimeline = ({
> >
{node.title} {node.title}
</TimelineTitle> </TimelineTitle>
{node.date && <TimelineDate>{node.date}</TimelineDate>}
</TimelineHeader> </TimelineHeader>
{node.content && <TimelineContent>{node.content}</TimelineContent>} {node.content && <TimelineContent>{node.content}</TimelineContent>}
</TimelineItem> </TimelineItem>

View File

@ -31,6 +31,7 @@ export interface ModalProps {
export interface ModalType extends FC<ModalProps> { export interface ModalType extends FC<ModalProps> {
show: typeof modalIns.show; show: typeof modalIns.show;
hide: typeof modalIns.hide; hide: typeof modalIns.hide;
destroy: typeof modalIns.destroy;
} }
const Modal: ModalType = ({ const Modal: ModalType = ({
@ -75,13 +76,13 @@ const Modal: ModalType = ({
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
onOpenChange?.(false); onOpenChange?.(false);
// onCancel?.(); onCancel?.();
}, [onOpenChange]); }, [onCancel, onOpenChange]);
const handleOk = useCallback(() => { const handleOk = useCallback(() => {
onOpenChange?.(true); onOpenChange?.(true);
// onOk?.(); onOk?.();
}, [onOpenChange]); }, [onOk, onOpenChange]);
const handleChange = (open: boolean) => { const handleChange = (open: boolean) => {
onOpenChange?.(open); onOpenChange?.(open);
console.log('open', open, onOpenChange); console.log('open', open, onOpenChange);
@ -208,5 +209,6 @@ Modal.show = modalIns
return modalIns.show; return modalIns.show;
}; };
Modal.hide = modalIns.hide; Modal.hide = modalIns.hide;
Modal.destroy = modalIns.destroy;
export { Modal }; export { Modal };

View File

@ -133,10 +133,10 @@ export const useNavigatePage = () => {
); );
const navigateToDataflowResult = useCallback( const navigateToDataflowResult = useCallback(
(id: string, knowledgeId?: string) => () => { (id: string, knowledgeId: string, doc_id?: string) => () => {
navigate( navigate(
// `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`, // `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`,
`${Routes.DataflowResult}?id=${id}&type=dataflow`, `${Routes.DataflowResult}?id=${id}&doc_id=${doc_id}&${QueryStringMap.KnowledgeId}=${knowledgeId}&type=dataflow`,
); );
}, },
[navigate], [navigate],

View File

@ -154,12 +154,7 @@ const ChunkerContainer = (props: IProps) => {
<RerunButton step={step} onRerun={handleReRunFunc} /> <RerunButton step={step} onRerun={handleReRunFunc} />
</div> </div>
)} )}
<div <div className={classNames('flex flex-col w-full')}>
className={classNames(
{ [styles.pagePdfWrapper]: isPdf },
'flex flex-col w-full',
)}
>
<Spin spinning={loading} className={styles.spin} size="large"> <Spin spinning={loading} className={styles.spin} size="large">
<div className="h-[50px] flex flex-row justify-between items-end pb-[5px]"> <div className="h-[50px] flex flex-row justify-between items-end pb-[5px]">
<div> <div>

View File

@ -1,24 +1,17 @@
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Ban, CircleCheck, Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
type ICheckboxSetProps = { type ICheckboxSetProps = {
selectAllChunk: (e: any) => void; selectAllChunk: (e: any) => void;
removeChunk: (e?: any) => void; removeChunk: (e?: any) => void;
switchChunk: (available: number) => void;
checked: boolean; checked: boolean;
selectedChunkIds: string[]; selectedChunkIds: string[];
}; };
export default (props: ICheckboxSetProps) => { export default (props: ICheckboxSetProps) => {
const { const { selectAllChunk, removeChunk, checked, selectedChunkIds } = props;
selectAllChunk,
removeChunk,
switchChunk,
checked,
selectedChunkIds,
} = props;
const { t } = useTranslation(); const { t } = useTranslation();
const handleSelectAllCheck = useCallback( const handleSelectAllCheck = useCallback(
(e: any) => { (e: any) => {
@ -32,14 +25,6 @@ export default (props: ICheckboxSetProps) => {
removeChunk(); removeChunk();
}, [removeChunk]); }, [removeChunk]);
const handleEnabledClick = useCallback(() => {
switchChunk(1);
}, [switchChunk]);
const handleDisabledClick = useCallback(() => {
switchChunk(0);
}, [switchChunk]);
const isSelected = useMemo(() => { const isSelected = useMemo(() => {
return selectedChunkIds?.length > 0; return selectedChunkIds?.length > 0;
}, [selectedChunkIds]); }, [selectedChunkIds]);
@ -57,20 +42,6 @@ export default (props: ICheckboxSetProps) => {
</div> </div>
{isSelected && ( {isSelected && (
<> <>
<div
className="flex items-center cursor-pointer text-muted-foreground hover:text-text-primary"
onClick={handleEnabledClick}
>
<CircleCheck size={16} />
<span className="block ml-1">{t('chunk.enable')}</span>
</div>
<div
className="flex items-center cursor-pointer text-muted-foreground hover:text-text-primary"
onClick={handleDisabledClick}
>
<Ban size={16} />
<span className="block ml-1">{t('chunk.disable')}</span>
</div>
<div <div
className="flex items-center cursor-pointer text-red-400 hover:text-red-500" className="flex items-center cursor-pointer text-red-400 hover:text-red-500"
onClick={handleDeleteClick} onClick={handleDeleteClick}

View File

@ -1,46 +1,168 @@
import { Checkbox } from '@/components/ui/checkbox';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CheckedState } from '@radix-ui/react-checkbox';
import { useState } from 'react'; import { useState } from 'react';
interface FormatPreserveEditorProps { interface FormatPreserveEditorProps {
initialValue: string; initialValue: {
onSave: (value: string) => void; key: string;
type: string;
value: Array<{ [key: string]: string }>;
};
onSave: (value: any) => void;
className?: string; className?: string;
isSelect?: boolean;
isDelete?: boolean;
isChunck?: boolean;
handleCheckboxClick?: (id: string | number, checked: boolean) => void;
selectedChunkIds?: string[];
} }
const FormatPreserveEditor = ({ const FormatPreserveEditor = ({
initialValue, initialValue,
onSave, onSave,
className, className,
isChunck,
handleCheckboxClick,
selectedChunkIds,
}: FormatPreserveEditorProps) => { }: FormatPreserveEditorProps) => {
const [content, setContent] = useState(initialValue); const [content, setContent] = useState(initialValue);
const [isEditing, setIsEditing] = useState(false); // const [isEditing, setIsEditing] = useState(false);
const [activeEditIndex, setActiveEditIndex] = useState<number | undefined>(
const handleEdit = () => setIsEditing(true); undefined,
);
const handleSave = () => { console.log('initialValue', initialValue);
onSave(content); const handleEdit = (e?: any, index?: number) => {
setIsEditing(false); console.log(e, index, content);
if (content.key === 'json') {
console.log(e, e.target.innerText);
setContent((pre) => ({
...pre,
value: pre.value.map((item, i) => {
if (i === index) {
return {
...item,
[Object.keys(item)[0]]: e.target.innerText,
};
}
return item;
}),
}));
setActiveEditIndex(index);
}
}; };
const handleChange = (e: any) => {
if (content.key === 'json') {
setContent((pre) => ({
...pre,
value: pre.value.map((item, i) => {
if (i === activeEditIndex) {
return {
...item,
[Object.keys(item)[0]]: e.target.value,
};
}
return item;
}),
}));
} else {
setContent(e.target.value);
}
};
const escapeNewlines = (text: string) => {
return text.replace(/\n/g, '\\n');
};
const unescapeNewlines = (text: string) => {
return text.replace(/\\n/g, '\n');
};
const handleSave = () => {
const saveData = {
...content,
value: content.value?.map((item) => {
return { ...item, text: unescapeNewlines(item.text) };
}),
};
onSave(saveData);
setActiveEditIndex(undefined);
};
const handleCheck = (e: CheckedState, id: string | number) => {
handleCheckboxClick?.(id, e === 'indeterminate' ? false : e);
};
return ( return (
<div className="editor-container"> <div className="editor-container">
{isEditing ? ( {/* {isEditing && content.key === 'json' ? (
<Textarea <Textarea
className={cn( className={cn(
'w-full h-full bg-transparent text-text-secondary', 'w-full h-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none min-h-6 p-0',
className, className,
)} )}
value={content} value={content.value}
onChange={(e) => setContent(e.target.value)} onChange={handleChange}
onBlur={handleSave} onBlur={handleSave}
autoSize={{ maxRows: 100 }} autoSize={{ maxRows: 100 }}
autoFocus autoFocus
/> />
) : ( ) : (
<pre className="text-text-secondary" onClick={handleEdit}> <>
{content} {content.key === 'json' && */}
</pre> {content.value.map((item, index) => (
)} <section
key={index}
className={
isChunck
? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start'
: ''
}
>
{isChunck && (
<Checkbox
onCheckedChange={(e) => {
handleCheck(e, index);
}}
checked={selectedChunkIds?.some(
(id) => id.toString() === index.toString(),
)}
></Checkbox>
)}
{activeEditIndex === index && (
<Textarea
key={'t' + index}
className={cn(
'w-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none !h-6 min-h-6 p-0',
className,
)}
value={escapeNewlines(content.value[index].text)}
onChange={handleChange}
onBlur={handleSave}
autoSize={{ maxRows: 100, minRows: 1 }}
autoFocus
/>
)}
{activeEditIndex !== index && (
<div
className="text-text-secondary overflow-auto scrollbar-auto whitespace-pre-wrap"
key={index}
onClick={(e) => {
handleEdit(e, index);
}}
>
{escapeNewlines(item.text)}
</div>
)}
</section>
))}
{/* {content.key !== 'json' && (
<pre
className="text-text-secondary overflow-auto scrollbar-auto"
onClick={handleEdit}
>
</pre>
)}
</>
)}*/}
</div> </div>
); );
}; };

View File

@ -4,16 +4,15 @@ import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal'; import { Modal } from '@/components/ui/modal/modal';
import { CircleAlert } from 'lucide-react'; import { CircleAlert } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRerunDataflow } from '../../hooks';
interface RerunButtonProps { interface RerunButtonProps {
className?: string; className?: string;
step?: TimelineNode; step?: TimelineNode;
onRerun?: () => void; onRerun?: () => void;
loading?: boolean;
} }
const RerunButton = (props: RerunButtonProps) => { const RerunButton = (props: RerunButtonProps) => {
const { className, step, onRerun } = props; const { className, step, onRerun, loading } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const { loading } = useRerunDataflow();
const clickFunc = () => { const clickFunc = () => {
console.log('click rerun button'); console.log('click rerun button');
Modal.show({ Modal.show({
@ -29,15 +28,15 @@ const RerunButton = (props: RerunButtonProps) => {
}} }}
></div> ></div>
), ),
onVisibleChange: () => { okText: t('modal.okText'),
Modal.hide(); cancelText: t('modal.cancelText'),
}, onVisibleChange: (visible: boolean) => {
onOk: () => { if (!visible) {
onRerun?.(); Modal.destroy();
Modal.hide(); } else {
}, onRerun?.();
onCancel: () => { Modal.destroy();
Modal.hide(); }
}, },
}); });
}; };

View File

@ -1,85 +1,112 @@
import { CustomTimeline, TimelineNode } from '@/components/originui/timeline'; import { CustomTimeline, TimelineNode } from '@/components/originui/timeline';
import { import {
CheckLine, Blocks,
FilePlayIcon, File,
Grid3x2, FilePlay,
FileStack,
Heading,
ListPlus, ListPlus,
PlayIcon,
} from 'lucide-react'; } from 'lucide-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
export enum TimelineNodeType { import { TimelineNodeType } from '../../constant';
begin = 'begin', import { IPipelineFileLogDetail } from '../../interface';
parser = 'parser',
chunk = 'chunk', export type ITimelineNodeObj = {
indexer = 'indexer', title: string;
complete = 'complete', icon: JSX.Element;
end = 'end', clickable?: boolean;
} type: TimelineNodeType;
export const TimelineNodeArr = [ };
{
id: 1, export const TimelineNodeObj = {
title: 'Begin', [TimelineNodeType.begin]: {
icon: <PlayIcon size={13} />, title: 'File',
icon: <File size={13} />,
clickable: false, clickable: false,
type: TimelineNodeType.begin,
}, },
{ [TimelineNodeType.parser]: {
id: 2,
title: 'Parser', title: 'Parser',
icon: <FilePlayIcon size={13} />, icon: <FilePlay size={13} />,
type: TimelineNodeType.parser,
}, },
{ [TimelineNodeType.contextGenerator]: {
id: 3, title: 'Context Generator',
title: 'Chunker', icon: <FileStack size={13} />,
icon: <Grid3x2 size={13} />,
type: TimelineNodeType.chunk,
}, },
{ [TimelineNodeType.titleSplitter]: {
id: 4, title: 'Title Splitter',
title: 'Indexer', icon: <Heading size={13} />,
},
[TimelineNodeType.characterSplitter]: {
title: 'Title Splitter',
icon: <Heading size={13} />,
},
[TimelineNodeType.splitter]: {
title: 'Character Splitter',
icon: <Blocks size={13} />,
},
[TimelineNodeType.tokenizer]: {
title: 'Tokenizer',
icon: <ListPlus size={13} />, icon: <ListPlus size={13} />,
clickable: false, clickable: false,
type: TimelineNodeType.indexer,
}, },
{ };
id: 5, // export const TimelineNodeArr = [
title: 'Complete', // {
icon: <CheckLine size={13} />, // id: 1,
clickable: false, // title: 'File',
type: TimelineNodeType.complete, // icon: <PlayIcon size={13} />,
}, // clickable: false,
]; // type: TimelineNodeType.begin,
// },
// {
// id: 2,
// title: 'Context Generator',
// icon: <PlayIcon size={13} />,
// type: TimelineNodeType.contextGenerator,
// },
// {
// id: 3,
// title: 'Title Splitter',
// icon: <PlayIcon size={13} />,
// type: TimelineNodeType.titleSplitter,
// },
// {
// id: 4,
// title: 'Character Splitter',
// icon: <PlayIcon size={13} />,
// type: TimelineNodeType.characterSplitter,
// },
// {
// id: 5,
// title: 'Tokenizer',
// icon: <CheckLine size={13} />,
// clickable: false,
// type: TimelineNodeType.tokenizer,
// },
// ]
export interface TimelineDataFlowProps { export interface TimelineDataFlowProps {
activeId: number | string; activeId: number | string;
activeFunc: (id: number | string, step: TimelineNode) => void; activeFunc: (id: number | string, step: TimelineNode) => void;
data: IPipelineFileLogDetail;
timelineNodes: TimelineNode[];
} }
const TimelineDataFlow = ({ activeFunc, activeId }: TimelineDataFlowProps) => { const TimelineDataFlow = ({
// const [activeStep, setActiveStep] = useState(2); activeFunc,
const timelineNodes: TimelineNode[] = useMemo(() => { activeId,
const nodes: TimelineNode[] = []; data,
TimelineNodeArr.forEach((node) => { timelineNodes,
nodes.push({ }: TimelineDataFlowProps) => {
...node, // const [timelineNodeArr,setTimelineNodeArr] = useState<ITimelineNodeObj & {id: number | string}>()
className: 'w-32',
completed: false,
});
});
return nodes;
}, []);
const activeStep = useMemo(() => { const activeStep = useMemo(() => {
const index = timelineNodes.findIndex((node) => node.id === activeId); const index = timelineNodes.findIndex((node) => node.id === activeId);
return index > -1 ? index + 1 : 0; return index > -1 ? index + 1 : 0;
}, [activeId, timelineNodes]); }, [activeId, timelineNodes]);
const handleStepChange = (step: number, id: string | number) => { const handleStepChange = (step: number, id: string | number) => {
// setActiveStep(step);
activeFunc?.( activeFunc?.(
id, id,
timelineNodes.find((node) => node.id === activeStep) as TimelineNode, timelineNodes.find((node) => node.id === activeStep) as TimelineNode,
); );
console.log(step, id);
}; };
return ( return (

View File

@ -2,3 +2,14 @@ export enum ChunkTextMode {
Full = 'full', Full = 'full',
Ellipse = 'ellipse', Ellipse = 'ellipse',
} }
export enum TimelineNodeType {
begin = 'file',
parser = 'parser',
splitter = 'splitter',
contextGenerator = 'contextGenerator',
titleSplitter = 'titleSplitter',
characterSplitter = 'characterSplitter',
tokenizer = 'tokenizer',
end = 'end',
}

View File

@ -1,4 +1,4 @@
import message from '@/components/ui/message'; import { TimelineNode } from '@/components/originui/timeline';
import { import {
useCreateChunk, useCreateChunk,
useDeleteChunk, useDeleteChunk,
@ -8,40 +8,17 @@ import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { IChunk } from '@/interfaces/database/knowledge'; import { IChunk } from '@/interfaces/database/knowledge';
import kbService from '@/services/knowledge-service'; import kbService from '@/services/knowledge-service';
import { formatSecondsToHumanReadable } from '@/utils/date';
import { buildChunkHighlights } from '@/utils/document-util'; import { buildChunkHighlights } from '@/utils/document-util';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { camelCase, upperFirst } from 'lodash';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IHighlight } from 'react-pdf-highlighter'; import { IHighlight } from 'react-pdf-highlighter';
import { useParams, useSearchParams } from 'umi'; import { useParams, useSearchParams } from 'umi';
import { ChunkTextMode } from './constant'; import { ITimelineNodeObj, TimelineNodeObj } from './components/time-line';
import { ChunkTextMode, TimelineNodeType } from './constant';
import { IDslComponent, IPipelineFileLogDetail } from './interface';
export interface IPipelineFileLogDetail {
avatar: string;
create_date: string;
create_time: number;
document_id: string;
document_name: string;
document_suffix: string;
document_type: string;
dsl: string;
id: string;
kb_id: string;
operation_status: string;
parser_id: string;
pipeline_id: string;
pipeline_title: string;
process_begin_at: 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 const useFetchPipelineFileLogDetail = (props?: { export const useFetchPipelineFileLogDetail = (props?: {
isEdit?: boolean; isEdit?: boolean;
refreshCount?: number; refreshCount?: number;
@ -199,52 +176,105 @@ export const useFetchParserList = () => {
}; };
}; };
export const useRerunDataflow = () => { export const useRerunDataflow = ({
data,
}: {
data: IPipelineFileLogDetail;
}) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isChange, setIsChange] = useState(false); const [isChange, setIsChange] = useState(false);
const handleReRunFunc = useCallback(
(newData: { value: IDslComponent; key: string }) => {
const newDsl = {
...data.dsl,
components: {
...data.dsl.components,
[newData.key]: newData.value,
},
};
// this Data provided to the interface
const params = {
id: data.id,
dsl: newDsl,
compenent_id: newData.key,
};
console.log('newDsl', newDsl, params);
},
[data],
);
return { return {
loading, loading,
setLoading, setLoading,
isChange, isChange,
setIsChange, setIsChange,
handleReRunFunc,
}; };
}; };
export const useFetchPaserText = () => { export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => {
const initialText = const timelineNodes: TimelineNode[] = useMemo(() => {
'第一行文本\n\t第二行缩进文本\n第三行 多个空格 第一行文本\n\t第二行缩进文本\n第三行 ' + const nodes: Array<ITimelineNodeObj & { id: number | string }> = [];
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + console.log('time-->', data);
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + const times = data?.dsl?.components;
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + if (times) {
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + const getNode = (
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + key: string,
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格'; index: number,
const [loading, setLoading] = useState(false); type:
const [data, setData] = useState<string>(initialText); | TimelineNodeType.begin
const { t } = useTranslation(); | TimelineNodeType.parser
const queryClient = useQueryClient(); | TimelineNodeType.splitter
| TimelineNodeType.tokenizer
| TimelineNodeType.characterSplitter
| TimelineNodeType.titleSplitter,
) => {
const node = times[key].obj;
const name = camelCase(
node.component_name,
) as keyof typeof TimelineNodeObj;
const { let tempType = type;
// data, if (name === TimelineNodeType.parser) {
// isPending: loading, tempType = TimelineNodeType.parser;
mutateAsync, } else if (name === TimelineNodeType.tokenizer) {
} = useMutation({ tempType = TimelineNodeType.tokenizer;
mutationKey: ['createChunk'], } else if (
mutationFn: async (payload: any) => { name === TimelineNodeType.characterSplitter ||
// let service = kbService.create_chunk; name === TimelineNodeType.titleSplitter ||
// if (payload.chunk_id) { name === TimelineNodeType.splitter
// service = kbService.set_chunk; ) {
// } tempType = TimelineNodeType.splitter;
// const { data } = await service(payload); }
// if (data.code === 0) { const timeNode = {
message.success(t('message.created')); ...TimelineNodeObj[name],
setTimeout(() => { clickable: true,
queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] }); id: index,
}, 1000); // Delay to ensure the list is updated className: 'w-32',
// } completed: false,
// return data?.code; date: formatSecondsToHumanReadable(
}, node.params?.outputs?._elapsed_time?.value || 0,
}); ),
type: tempType,
detail: { value: times[key], key: key },
};
console.log('timeNodetype-->', type);
nodes.push(timeNode);
return { data, loading, rerun: mutateAsync }; if (times[key].downstream && times[key].downstream.length > 0) {
const nextKey = times[key].downstream[0];
// nodes.push(timeNode);
getNode(nextKey, index + 1, tempType);
}
};
getNode(upperFirst(TimelineNodeType.begin), 1, TimelineNodeType.begin);
// setTimelineNodeArr(nodes as unknown as ITimelineNodeObj & {id: number | string})
}
return nodes;
}, [data]);
return {
timelineNodes,
};
}; };

View File

@ -7,6 +7,7 @@ import {
useGetChunkHighlights, useGetChunkHighlights,
useHandleChunkCardClick, useHandleChunkCardClick,
useRerunDataflow, useRerunDataflow,
useTimelineDataFlow,
} from './hooks'; } from './hooks';
import DocumentHeader from './components/document-preview/document-header'; import DocumentHeader from './components/document-preview/document-header';
@ -29,13 +30,11 @@ import {
useNavigatePage, useNavigatePage,
} from '@/hooks/logic-hooks/navigate-hooks'; } from '@/hooks/logic-hooks/navigate-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { ChunkerContainer } from './chunker';
import { useGetDocumentUrl } from './components/document-preview/hooks'; import { useGetDocumentUrl } from './components/document-preview/hooks';
import TimelineDataFlow, { import TimelineDataFlow from './components/time-line';
TimelineNodeArr, import { TimelineNodeType } from './constant';
TimelineNodeType,
} from './components/time-line';
import styles from './index.less'; import styles from './index.less';
import { IDslComponent } from './interface';
import ParserContainer from './parser'; import ParserContainer from './parser';
const Chunk = () => { const Chunk = () => {
@ -45,9 +44,10 @@ const Chunk = () => {
const { selectedChunkId } = useHandleChunkCardClick(); const { selectedChunkId } = useHandleChunkCardClick();
const [activeStepId, setActiveStepId] = useState<number | string>(0); const [activeStepId, setActiveStepId] = useState<number | string>(0);
const { data: dataset } = useFetchPipelineFileLogDetail(); const { data: dataset } = useFetchPipelineFileLogDetail();
const { isChange, setIsChange } = useRerunDataflow();
const { t } = useTranslation(); const { t } = useTranslation();
const { timelineNodes } = useTimelineDataFlow(dataset);
const { navigateToDataset, getQueryString, navigateToDatasetList } = const { navigateToDataset, getQueryString, navigateToDatasetList } =
useNavigatePage(); useNavigatePage();
const fileUrl = useGetDocumentUrl(); const fileUrl = useGetDocumentUrl();
@ -69,8 +69,15 @@ const Chunk = () => {
return 'unknown'; return 'unknown';
}, [documentInfo]); }, [documentInfo]);
const {
handleReRunFunc,
isChange,
setIsChange,
loading: reRunLoading,
} = useRerunDataflow({
data: dataset,
});
const handleStepChange = (id: number | string, step: TimelineNode) => { const handleStepChange = (id: number | string, step: TimelineNode) => {
console.log(id, step);
if (isChange) { if (isChange) {
Modal.show({ Modal.show({
visible: true, visible: true,
@ -114,12 +121,14 @@ const Chunk = () => {
}; };
const { type } = useGetKnowledgeSearchParams(); const { type } = useGetKnowledgeSearchParams();
const currentTimeNode: TimelineNode = useMemo(() => { const currentTimeNode: TimelineNode = useMemo(() => {
return ( return (
TimelineNodeArr.find((node) => node.id === activeStepId) || timelineNodes.find((node) => node.id === activeStepId) ||
({} as TimelineNode) ({} as TimelineNode)
); );
}, [activeStepId]); }, [activeStepId, timelineNodes]);
return ( return (
<> <>
<PageHeader> <PageHeader>
@ -134,10 +143,10 @@ const Chunk = () => {
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink <BreadcrumbLink
onClick={navigateToDataset( onClick={navigateToDataset(
getQueryString(QueryStringMap.id) as string, getQueryString(QueryStringMap.KnowledgeId) as string,
)} )}
> >
{dataset.name} {t('knowledgeDetails.overview')}
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
@ -152,6 +161,8 @@ const Chunk = () => {
<TimelineDataFlow <TimelineDataFlow
activeFunc={handleStepChange} activeFunc={handleStepChange}
activeId={activeStepId} activeId={activeStepId}
data={dataset}
timelineNodes={timelineNodes}
/> />
</div> </div>
)} )}
@ -173,20 +184,31 @@ const Chunk = () => {
</div> </div>
<div className="h-dvh border-r -mt-3"></div> <div className="h-dvh border-r -mt-3"></div>
<div className="w-3/5 h-full"> <div className="w-3/5 h-full">
{currentTimeNode?.type === TimelineNodeType.chunk && ( {/* {currentTimeNode?.type === TimelineNodeType.splitter && (
<ChunkerContainer <ChunkerContainer
isChange={isChange} isChange={isChange}
setIsChange={setIsChange} setIsChange={setIsChange}
step={currentTimeNode as TimelineNode} step={currentTimeNode as TimelineNode}
/> />
)} )} */}
{currentTimeNode?.type === TimelineNodeType.parser && ( {/* {currentTimeNode?.type === TimelineNodeType.parser && ( */}
{(currentTimeNode?.type === TimelineNodeType.parser ||
currentTimeNode?.type === TimelineNodeType.splitter) && (
<ParserContainer <ParserContainer
isChange={isChange} isChange={isChange}
reRunLoading={reRunLoading}
setIsChange={setIsChange} setIsChange={setIsChange}
step={currentTimeNode as TimelineNode} step={currentTimeNode as TimelineNode}
data={
currentTimeNode.detail as {
value: IDslComponent;
key: string;
}
}
reRunFunc={handleReRunFunc}
/> />
)} )}
{/* )} */}
<Spotlight opcity={0.6} coverage={60} /> <Spotlight opcity={0.6} coverage={60} />
</div> </div>
</div> </div>

View File

@ -0,0 +1,62 @@
interface ComponentParams {
debug_inputs: Record<string, any>;
delay_after_error: number;
description: string;
exception_default_value: any;
exception_goto: any;
exception_method: any;
inputs: Record<string, any>;
max_retries: number;
message_history_window_size: number;
outputs: {
_created_time: Record<string, any>;
_elapsed_time: Record<string, any>;
name: Record<string, any>;
output_format: { type: string; value: string };
json: { type: string; value: string };
};
persist_logs: boolean;
timeout: number;
}
interface ComponentObject {
component_name: string;
params: ComponentParams;
}
export interface IDslComponent {
downstream: Array<string>;
obj: ComponentObject;
upstream: Array<string>;
}
export interface IPipelineFileLogDetail {
avatar: string;
create_date: string;
create_time: number;
document_id: string;
document_name: string;
document_suffix: string;
document_type: string;
dsl: {
components: {
[key: string]: IDslComponent;
};
task_id: string;
path: Array<string>;
};
id: string;
kb_id: string;
operation_status: string;
parser_id: string;
pipeline_id: string;
pipeline_title: string;
process_begin_at: 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;
}

View File

@ -2,41 +2,121 @@ import { TimelineNode } from '@/components/originui/timeline';
import Spotlight from '@/components/spotlight'; import Spotlight from '@/components/spotlight';
import { Spin } from '@/components/ui/spin'; import { Spin } from '@/components/ui/spin';
import classNames from 'classnames'; import classNames from 'classnames';
import { useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import FormatPreserveEditor from './components/parse-editer'; import CheckboxSets from './components/chunk-result-bar/checkbox-sets';
import FormatPreserEditor from './components/parse-editer';
import RerunButton from './components/rerun-button'; import RerunButton from './components/rerun-button';
import { useFetchParserList, useFetchPaserText } from './hooks'; import { TimelineNodeType } from './constant';
import { useFetchParserList } from './hooks';
import { IDslComponent } from './interface';
interface IProps { interface IProps {
isChange: boolean; isChange: boolean;
setIsChange: (isChange: boolean) => void; setIsChange: (isChange: boolean) => void;
step?: TimelineNode; step?: TimelineNode;
data: { value: IDslComponent; key: string };
reRunLoading: boolean;
reRunFunc: (data: { value: IDslComponent; key: string }) => void;
} }
const ParserContainer = (props: IProps) => { const ParserContainer = (props: IProps) => {
const { isChange, setIsChange, step } = props; const { isChange, setIsChange, step, data, reRunFunc, reRunLoading } = props;
const { data: initialValue, rerun: onSave } = useFetchPaserText();
const { t } = useTranslation(); const { t } = useTranslation();
const { loading } = useFetchParserList(); const { loading } = useFetchParserList();
const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]);
const initialValue = useMemo(() => {
const outputs = data?.value?.obj?.params?.outputs;
const key = outputs?.output_format?.value;
const value = outputs[key]?.value;
const type = outputs[key]?.type;
console.log('outputs-->', outputs);
return {
key,
type,
value,
};
}, [data]);
const [initialText, setInitialText] = useState(initialValue); const [initialText, setInitialText] = useState(initialValue);
const handleSave = (newContent: string) => { const handleSave = (newContent: any) => {
console.log('保存内容:', newContent); console.log('newContent-change-->', newContent, initialValue);
if (newContent !== initialText) { if (JSON.stringify(newContent) !== JSON.stringify(initialValue)) {
setIsChange(true); setIsChange(true);
onSave(newContent); setInitialText(newContent);
} else { } else {
setIsChange(false); setIsChange(false);
} }
// Here, the API is called to send newContent to the backend // Here, the API is called to send newContent to the backend
}; };
const handleReRunFunc = () => {
const handleReRunFunc = useCallback(() => {
const newData: { value: IDslComponent; key: string } = {
...data,
value: {
...data.value,
obj: {
...data.value.obj,
params: {
...(data.value?.obj?.params || {}),
outputs: {
...(data.value?.obj?.params?.outputs || {}),
[initialText.key]: {
type: initialText.type,
value: initialText.value,
},
},
},
},
},
};
reRunFunc(newData);
setIsChange(false); setIsChange(false);
}; }, [data, initialText, reRunFunc, setIsChange]);
const handleRemoveChunk = useCallback(async () => {
if (selectedChunkIds.length > 0) {
initialText.value = initialText.value.filter(
(item: any, index: number) => !selectedChunkIds.includes(index + ''),
);
setSelectedChunkIds([]);
}
}, [selectedChunkIds, initialText]);
const handleCheckboxClick = useCallback(
(id: string | number, checked: boolean) => {
console.log('handleCheckboxClick', id, checked, selectedChunkIds);
setSelectedChunkIds((prev) => {
if (checked) {
return [...prev, id.toString()];
} else {
return prev.filter((item) => item.toString() !== id.toString());
}
});
},
[],
);
const selectAllChunk = useCallback(
(checked: boolean) => {
setSelectedChunkIds(
checked ? initialText.value.map((x, index: number) => index) : [],
);
},
[initialText.value],
);
const isChunck =
step?.type === TimelineNodeType.characterSplitter ||
step?.type === TimelineNodeType.titleSplitter ||
step?.type === TimelineNodeType.splitter;
return ( return (
<> <>
{isChange && ( {isChange && (
<div className=" absolute top-2 right-6"> <div className=" absolute top-2 right-6">
<RerunButton step={step} onRerun={handleReRunFunc} /> <RerunButton
step={step}
onRerun={handleReRunFunc}
loading={reRunLoading}
/>
</div> </div>
)} )}
<div className={classNames('flex flex-col w-full')}> <div className={classNames('flex flex-col w-full')}>
@ -51,11 +131,33 @@ const ParserContainer = (props: IProps) => {
</div> </div>
</div> </div>
</div> </div>
<div className=" border rounded-lg p-[20px] box-border h-[calc(100vh-180px)] overflow-auto scrollbar-none">
<FormatPreserveEditor {isChunck && (
<div className="pt-[5px] pb-[5px]">
<CheckboxSets
selectAllChunk={selectAllChunk}
removeChunk={handleRemoveChunk}
checked={selectedChunkIds.length === initialText.value.length}
selectedChunkIds={selectedChunkIds}
/>
</div>
)}
<div className=" border rounded-lg p-[20px] box-border h-[calc(100vh-180px)] w-[calc(100%-20px)] overflow-auto scrollbar-none">
<FormatPreserEditor
initialValue={initialText} initialValue={initialText}
onSave={handleSave} onSave={handleSave}
className="!h-[calc(100vh-220px)]" className={
initialText.key !== 'json' ? '!h-[calc(100vh-220px)]' : ''
}
isChunck={isChunck}
isDelete={
step?.type === TimelineNodeType.characterSplitter ||
step?.type === TimelineNodeType.titleSplitter ||
step?.type === TimelineNodeType.splitter
}
handleCheckboxClick={handleCheckboxClick}
selectedChunkIds={selectedChunkIds}
/> />
<Spotlight opcity={0.6} coverage={60} /> <Spotlight opcity={0.6} coverage={60} />
</div> </div>

View File

@ -4,10 +4,12 @@ import {
} from '@/hooks/logic-hooks'; } from '@/hooks/logic-hooks';
import kbService, { import kbService, {
listDataPipelineLogDocument, listDataPipelineLogDocument,
listPipelineDatasetLogs,
} from '@/services/knowledge-service'; } from '@/services/knowledge-service';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react'; import { useCallback, useState } from 'react';
import { useParams, useSearchParams } from 'umi'; import { useParams, useSearchParams } from 'umi';
import { LogTabs } from './dataset-common';
export interface IOverviewTital { export interface IOverviewTital {
cancelled: number; cancelled: number;
@ -61,7 +63,7 @@ export interface IFileLogItem {
update_time: number; update_time: number;
} }
export interface IFileLogList { export interface IFileLogList {
docs: IFileLogItem[]; logs: IFileLogItem[];
total: number; total: number;
} }
@ -70,7 +72,14 @@ const useFetchFileLogList = () => {
const { searchString, handleInputChange } = useHandleSearchChange(); const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter(); const { pagination, setPagination } = useGetPaginationWithRouter();
const { id } = useParams(); const { id } = useParams();
const [active, setActive] = useState<(typeof LogTabs)[keyof typeof LogTabs]>(
LogTabs.FILE_LOGS,
);
const knowledgeBaseId = searchParams.get('id') || id; const knowledgeBaseId = searchParams.get('id') || id;
const fetchFunc =
active === LogTabs.DATASET_LOGS
? listPipelineDatasetLogs
: listDataPipelineLogDocument;
const { data } = useQuery<IFileLogList>({ const { data } = useQuery<IFileLogList>({
queryKey: [ queryKey: [
'fileLogList', 'fileLogList',
@ -78,9 +87,10 @@ const useFetchFileLogList = () => {
pagination.current, pagination.current,
pagination.pageSize, pagination.pageSize,
searchString, searchString,
active,
], ],
queryFn: async () => { queryFn: async () => {
const { data: res = {} } = await listDataPipelineLogDocument({ const { data: res = {} } = await fetchFunc({
kb_id: knowledgeBaseId, kb_id: knowledgeBaseId,
page: pagination.current, page: pagination.current,
page_size: pagination.pageSize, page_size: pagination.pageSize,
@ -102,6 +112,8 @@ const useFetchFileLogList = () => {
searchString, searchString,
handleInputChange: onInputChange, handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.total }, pagination: { ...pagination, total: data?.total },
active,
setActive,
}; };
}; };

View File

@ -71,9 +71,7 @@ const CardFooterProcess: FC<CardFooterProcessProps> = ({
}; };
const FileLogsPage: FC = () => { const FileLogsPage: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [active, setActive] = useState<(typeof LogTabs)[keyof typeof LogTabs]>(
LogTabs.FILE_LOGS,
);
const [topAllData, setTopAllData] = useState({ const [topAllData, setTopAllData] = useState({
totalFiles: { totalFiles: {
value: 0, value: 0,
@ -111,12 +109,14 @@ const FileLogsPage: FC = () => {
searchString, searchString,
handleInputChange, handleInputChange,
pagination, pagination,
active,
setActive,
} = useFetchFileLogList(); } = useFetchFileLogList();
const tableList = useMemo(() => { const tableList = useMemo(() => {
console.log('tableList', tableOriginData); console.log('tableList', tableOriginData);
if (tableOriginData && tableOriginData.docs?.length) { if (tableOriginData && tableOriginData.logs?.length) {
return tableOriginData.docs.map((item) => { return tableOriginData.logs.map((item) => {
return { return {
...item, ...item,
fileName: item.document_name, fileName: item.document_name,

View File

@ -31,6 +31,7 @@ import {
import { TFunction } from 'i18next'; import { TFunction } from 'i18next';
import { ClipboardList, Eye } from 'lucide-react'; import { ClipboardList, Eye } from 'lucide-react';
import { FC, useMemo, useState } from 'react'; import { FC, useMemo, useState } from 'react';
import { useParams } from 'umi';
import { RunningStatus } from '../dataset/constant'; import { RunningStatus } from '../dataset/constant';
import ProcessLogModal from '../process-log-modal'; import ProcessLogModal from '../process-log-modal';
import { LogTabs, ProcessingType } from './dataset-common'; import { LogTabs, ProcessingType } from './dataset-common';
@ -58,9 +59,11 @@ interface FileLogsTableProps {
export const getFileLogsTableColumns = ( export const getFileLogsTableColumns = (
t: TFunction<'translation', string>, t: TFunction<'translation', string>,
showLog: (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => void, showLog: (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => void,
kowledgeId: string,
navigateToDataflowResult: ( navigateToDataflowResult: (
id: string, id: string,
knowledgeId?: string | undefined, knowledgeId: string,
doc_id?: string,
) => () => void, ) => () => void,
) => { ) => {
// const { t } = useTranslate('knowledgeDetails'); // const { t } = useTranslate('knowledgeDetails');
@ -175,7 +178,11 @@ export const getFileLogsTableColumns = (
variant="ghost" variant="ghost"
size="sm" size="sm"
className="p-1" className="p-1"
onClick={navigateToDataflowResult(row.original.id)} onClick={navigateToDataflowResult(
row.original.id,
kowledgeId,
row.original.document_id,
)}
> >
<ClipboardList /> <ClipboardList />
</Button> </Button>
@ -288,7 +295,8 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const { navigateToDataflowResult } = useNavigatePage(); const { navigateToDataflowResult } = useNavigatePage();
const [logInfo, setLogInfo] = useState<IFileLogItem>({}); const [logInfo, setLogInfo] = useState<IFileLogItem>({});
const showLog = (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => { const kowledgeId = useParams().id;
const showLog = (row: Row<IFileLogItem & DocumentLog>) => {
const logDetail = { const logDetail = {
taskId: row.original.id, taskId: row.original.id,
fileName: row.original.document_name, fileName: row.original.document_name,
@ -306,7 +314,12 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
const columns = useMemo(() => { const columns = useMemo(() => {
return active === LogTabs.FILE_LOGS return active === LogTabs.FILE_LOGS
? getFileLogsTableColumns(t, showLog, navigateToDataflowResult) ? getFileLogsTableColumns(
t,
showLog,
kowledgeId || '',
navigateToDataflowResult,
)
: getDatasetLogsTableColumns(t, showLog); : getDatasetLogsTableColumns(t, showLog);
}, [active, t]); }, [active, t]);

View File

@ -80,7 +80,7 @@ export const useShowLog = (documents: IDocumentInfo[]) => {
fileSize: findRecord.size + '', fileSize: findRecord.size + '',
source: findRecord.source_type, source: findRecord.source_type,
task: findRecord.status, task: findRecord.status,
state: findRecord.run, status: findRecord.run,
startTime: findRecord.process_begin_at, startTime: findRecord.process_begin_at,
endTime: findRecord.process_begin_at, endTime: findRecord.process_begin_at,
duration: findRecord.process_duration + 's', duration: findRecord.process_duration + 's',

View File

@ -7,11 +7,6 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { IDocumentInfo } from '@/interfaces/database/document'; import { IDocumentInfo } from '@/interfaces/database/document';
@ -19,7 +14,7 @@ import { CircleX } from 'lucide-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DocumentType, RunningStatus } from './constant'; import { DocumentType, RunningStatus } from './constant';
import { ParsingCard, PopoverContent } from './parsing-card'; import { ParsingCard } from './parsing-card';
import { UseChangeDocumentParserShowType } from './use-change-document-parser'; import { UseChangeDocumentParserShowType } from './use-change-document-parser';
import { useHandleRunDocumentByIds } from './use-run-document'; import { useHandleRunDocumentByIds } from './use-run-document';
import { UseSaveMetaShowType } from './use-save-meta'; import { UseSaveMetaShowType } from './use-save-meta';
@ -122,23 +117,13 @@ export function ParsingStatusCell({
)} )}
{isParserRunning(run) ? ( {isParserRunning(run) ? (
<> <>
<HoverCard> <div
<HoverCardTrigger asChild> className="flex items-center gap-1 cursor-pointer"
<div onClick={() => handleShowLog(record)}
className="flex items-center gap-1" >
onClick={() => handleShowLog(record)} <Progress value={p} className="h-1 flex-1 min-w-10" />
> {p}%
<Progress value={p} className="h-1 flex-1 min-w-10" /> </div>
{p}%
</div>
</HoverCardTrigger>
<HoverCardContent className="w-[40vw]">
<PopoverContent
record={record}
handleShowLog={handleShowLog}
></PopoverContent>
</HoverCardContent>
</HoverCard>
<div <div
className="cursor-pointer flex items-center gap-3" className="cursor-pointer flex items-center gap-3"
onClick={ onClick={

View File

@ -41,6 +41,7 @@ const {
retrievalTestShare, retrievalTestShare,
getKnowledgeBasicInfo, getKnowledgeBasicInfo,
fetchDataPipelineLog, fetchDataPipelineLog,
fetchPipelineDatasetLogs,
} = api; } = api;
const methods = { const methods = {
@ -179,6 +180,10 @@ const methods = {
url: fetchDataPipelineLog, url: fetchDataPipelineLog,
method: 'post', method: 'post',
}, },
fetchPipelineDatasetLogs: {
url: fetchPipelineDatasetLogs,
method: 'post',
},
get_pipeline_detail: { get_pipeline_detail: {
url: api.get_pipeline_detail, url: api.get_pipeline_detail,
method: 'get', method: 'get',
@ -223,5 +228,9 @@ export const listDataPipelineLogDocument = (
params?: IFetchKnowledgeListRequestParams, params?: IFetchKnowledgeListRequestParams,
body?: IFetchDocumentListRequestBody, body?: IFetchDocumentListRequestBody,
) => request.post(api.fetchDataPipelineLog, { data: body || {}, params }); ) => request.post(api.fetchDataPipelineLog, { data: body || {}, params });
export const listPipelineDatasetLogs = (
params?: IFetchKnowledgeListRequestParams,
body?: IFetchDocumentListRequestBody,
) => request.post(api.fetchPipelineDatasetLogs, { data: body || {}, params });
export default kbService; export default kbService;

View File

@ -49,6 +49,7 @@ export default {
// data pipeline log // data pipeline log
fetchDataPipelineLog: `${api_host}/kb/list_pipeline_logs`, fetchDataPipelineLog: `${api_host}/kb/list_pipeline_logs`,
get_pipeline_detail: `${api_host}/kb/pipeline_log_detail`, get_pipeline_detail: `${api_host}/kb/pipeline_log_detail`,
fetchPipelineDatasetLogs: `${api_host}/kb/list_pipeline_dataset_logs`,
// tags // tags
listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`, listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`,

View File

@ -1,4 +1,5 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { toFixed } from './common-util';
export function formatDate(date: any) { export function formatDate(date: any) {
if (!date) { if (!date) {
@ -43,3 +44,20 @@ export function formatStandardDate(date: any) {
} }
return parsedDate.format('YYYY-MM-DD'); return parsedDate.format('YYYY-MM-DD');
} }
export function formatSecondsToHumanReadable(seconds: number): string {
if (isNaN(seconds) || seconds < 0) {
return '0s';
}
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = toFixed(seconds % 60, 3);
const parts = [];
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
if (s || parts.length === 0) parts.push(`${s}s`);
return parts.join('');
}