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

View File

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

View File

@ -133,10 +133,10 @@ export const useNavigatePage = () => {
);
const navigateToDataflowResult = useCallback(
(id: string, knowledgeId?: string) => () => {
(id: string, knowledgeId: string, doc_id?: string) => () => {
navigate(
// `${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],

View File

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

View File

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

View File

@ -1,46 +1,168 @@
import { Checkbox } from '@/components/ui/checkbox';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { CheckedState } from '@radix-ui/react-checkbox';
import { useState } from 'react';
interface FormatPreserveEditorProps {
initialValue: string;
onSave: (value: string) => void;
initialValue: {
key: string;
type: string;
value: Array<{ [key: string]: string }>;
};
onSave: (value: any) => void;
className?: string;
isSelect?: boolean;
isDelete?: boolean;
isChunck?: boolean;
handleCheckboxClick?: (id: string | number, checked: boolean) => void;
selectedChunkIds?: string[];
}
const FormatPreserveEditor = ({
initialValue,
onSave,
className,
isChunck,
handleCheckboxClick,
selectedChunkIds,
}: FormatPreserveEditorProps) => {
const [content, setContent] = useState(initialValue);
const [isEditing, setIsEditing] = useState(false);
const handleEdit = () => setIsEditing(true);
const handleSave = () => {
onSave(content);
setIsEditing(false);
// const [isEditing, setIsEditing] = useState(false);
const [activeEditIndex, setActiveEditIndex] = useState<number | undefined>(
undefined,
);
console.log('initialValue', initialValue);
const handleEdit = (e?: any, index?: number) => {
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 (
<div className="editor-container">
{isEditing ? (
{/* {isEditing && content.key === 'json' ? (
<Textarea
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,
)}
value={content}
onChange={(e) => setContent(e.target.value)}
value={content.value}
onChange={handleChange}
onBlur={handleSave}
autoSize={{ maxRows: 100 }}
autoFocus
/>
) : (
<pre className="text-text-secondary" onClick={handleEdit}>
{content}
<>
{content.key === 'json' && */}
{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>
);
};

View File

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

View File

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

View File

@ -2,3 +2,14 @@ export enum ChunkTextMode {
Full = 'full',
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 {
useCreateChunk,
useDeleteChunk,
@ -8,40 +8,17 @@ import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { IChunk } from '@/interfaces/database/knowledge';
import kbService from '@/services/knowledge-service';
import { formatSecondsToHumanReadable } from '@/utils/date';
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 { useTranslation } from 'react-i18next';
import { IHighlight } from 'react-pdf-highlighter';
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?: {
isEdit?: boolean;
refreshCount?: number;
@ -199,52 +176,105 @@ export const useFetchParserList = () => {
};
};
export const useRerunDataflow = () => {
export const useRerunDataflow = ({
data,
}: {
data: IPipelineFileLogDetail;
}) => {
const [loading, setLoading] = 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 {
loading,
setLoading,
isChange,
setIsChange,
handleReRunFunc,
};
};
export const useFetchPaserText = () => {
const initialText =
'第一行文本\n\t第二行缩进文本\n第三行 多个空格 第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格';
const [loading, setLoading] = useState(false);
const [data, setData] = useState<string>(initialText);
const { t } = useTranslation();
const queryClient = useQueryClient();
export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => {
const timelineNodes: TimelineNode[] = useMemo(() => {
const nodes: Array<ITimelineNodeObj & { id: number | string }> = [];
console.log('time-->', data);
const times = data?.dsl?.components;
if (times) {
const getNode = (
key: string,
index: number,
type:
| TimelineNodeType.begin
| TimelineNodeType.parser
| TimelineNodeType.splitter
| TimelineNodeType.tokenizer
| TimelineNodeType.characterSplitter
| TimelineNodeType.titleSplitter,
) => {
const node = times[key].obj;
const name = camelCase(
node.component_name,
) as keyof typeof TimelineNodeObj;
const {
// data,
// isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['createChunk'],
mutationFn: async (payload: any) => {
// let service = kbService.create_chunk;
// if (payload.chunk_id) {
// service = kbService.set_chunk;
// }
// const { data } = await service(payload);
// if (data.code === 0) {
message.success(t('message.created'));
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] });
}, 1000); // Delay to ensure the list is updated
// }
// return data?.code;
},
});
return { data, loading, rerun: mutateAsync };
let tempType = type;
if (name === TimelineNodeType.parser) {
tempType = TimelineNodeType.parser;
} else if (name === TimelineNodeType.tokenizer) {
tempType = TimelineNodeType.tokenizer;
} else if (
name === TimelineNodeType.characterSplitter ||
name === TimelineNodeType.titleSplitter ||
name === TimelineNodeType.splitter
) {
tempType = TimelineNodeType.splitter;
}
const timeNode = {
...TimelineNodeObj[name],
clickable: true,
id: index,
className: 'w-32',
completed: false,
date: formatSecondsToHumanReadable(
node.params?.outputs?._elapsed_time?.value || 0,
),
type: tempType,
detail: { value: times[key], key: key },
};
console.log('timeNodetype-->', type);
nodes.push(timeNode);
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,
useHandleChunkCardClick,
useRerunDataflow,
useTimelineDataFlow,
} from './hooks';
import DocumentHeader from './components/document-preview/document-header';
@ -29,13 +30,11 @@ import {
useNavigatePage,
} from '@/hooks/logic-hooks/navigate-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { ChunkerContainer } from './chunker';
import { useGetDocumentUrl } from './components/document-preview/hooks';
import TimelineDataFlow, {
TimelineNodeArr,
TimelineNodeType,
} from './components/time-line';
import TimelineDataFlow from './components/time-line';
import { TimelineNodeType } from './constant';
import styles from './index.less';
import { IDslComponent } from './interface';
import ParserContainer from './parser';
const Chunk = () => {
@ -45,9 +44,10 @@ const Chunk = () => {
const { selectedChunkId } = useHandleChunkCardClick();
const [activeStepId, setActiveStepId] = useState<number | string>(0);
const { data: dataset } = useFetchPipelineFileLogDetail();
const { isChange, setIsChange } = useRerunDataflow();
const { t } = useTranslation();
const { timelineNodes } = useTimelineDataFlow(dataset);
const { navigateToDataset, getQueryString, navigateToDatasetList } =
useNavigatePage();
const fileUrl = useGetDocumentUrl();
@ -69,8 +69,15 @@ const Chunk = () => {
return 'unknown';
}, [documentInfo]);
const {
handleReRunFunc,
isChange,
setIsChange,
loading: reRunLoading,
} = useRerunDataflow({
data: dataset,
});
const handleStepChange = (id: number | string, step: TimelineNode) => {
console.log(id, step);
if (isChange) {
Modal.show({
visible: true,
@ -114,12 +121,14 @@ const Chunk = () => {
};
const { type } = useGetKnowledgeSearchParams();
const currentTimeNode: TimelineNode = useMemo(() => {
return (
TimelineNodeArr.find((node) => node.id === activeStepId) ||
timelineNodes.find((node) => node.id === activeStepId) ||
({} as TimelineNode)
);
}, [activeStepId]);
}, [activeStepId, timelineNodes]);
return (
<>
<PageHeader>
@ -134,10 +143,10 @@ const Chunk = () => {
<BreadcrumbItem>
<BreadcrumbLink
onClick={navigateToDataset(
getQueryString(QueryStringMap.id) as string,
getQueryString(QueryStringMap.KnowledgeId) as string,
)}
>
{dataset.name}
{t('knowledgeDetails.overview')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
@ -152,6 +161,8 @@ const Chunk = () => {
<TimelineDataFlow
activeFunc={handleStepChange}
activeId={activeStepId}
data={dataset}
timelineNodes={timelineNodes}
/>
</div>
)}
@ -173,20 +184,31 @@ const Chunk = () => {
</div>
<div className="h-dvh border-r -mt-3"></div>
<div className="w-3/5 h-full">
{currentTimeNode?.type === TimelineNodeType.chunk && (
{/* {currentTimeNode?.type === TimelineNodeType.splitter && (
<ChunkerContainer
isChange={isChange}
setIsChange={setIsChange}
step={currentTimeNode as TimelineNode}
/>
)}
{currentTimeNode?.type === TimelineNodeType.parser && (
)} */}
{/* {currentTimeNode?.type === TimelineNodeType.parser && ( */}
{(currentTimeNode?.type === TimelineNodeType.parser ||
currentTimeNode?.type === TimelineNodeType.splitter) && (
<ParserContainer
isChange={isChange}
reRunLoading={reRunLoading}
setIsChange={setIsChange}
step={currentTimeNode as TimelineNode}
data={
currentTimeNode.detail as {
value: IDslComponent;
key: string;
}
}
reRunFunc={handleReRunFunc}
/>
)}
{/* )} */}
<Spotlight opcity={0.6} coverage={60} />
</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 { Spin } from '@/components/ui/spin';
import classNames from 'classnames';
import { useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
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 { useFetchParserList, useFetchPaserText } from './hooks';
import { TimelineNodeType } from './constant';
import { useFetchParserList } from './hooks';
import { IDslComponent } from './interface';
interface IProps {
isChange: boolean;
setIsChange: (isChange: boolean) => void;
step?: TimelineNode;
data: { value: IDslComponent; key: string };
reRunLoading: boolean;
reRunFunc: (data: { value: IDslComponent; key: string }) => void;
}
const ParserContainer = (props: IProps) => {
const { isChange, setIsChange, step } = props;
const { data: initialValue, rerun: onSave } = useFetchPaserText();
const { isChange, setIsChange, step, data, reRunFunc, reRunLoading } = props;
const { t } = useTranslation();
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 handleSave = (newContent: string) => {
console.log('保存内容:', newContent);
if (newContent !== initialText) {
const handleSave = (newContent: any) => {
console.log('newContent-change-->', newContent, initialValue);
if (JSON.stringify(newContent) !== JSON.stringify(initialValue)) {
setIsChange(true);
onSave(newContent);
setInitialText(newContent);
} else {
setIsChange(false);
}
// Here, the API is called to send newContent to the backend
};
const handleReRunFunc = () => {
setIsChange(false);
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);
}, [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 (
<>
{isChange && (
<div className=" absolute top-2 right-6">
<RerunButton step={step} onRerun={handleReRunFunc} />
<RerunButton
step={step}
onRerun={handleReRunFunc}
loading={reRunLoading}
/>
</div>
)}
<div className={classNames('flex flex-col w-full')}>
@ -51,11 +131,33 @@ const ParserContainer = (props: IProps) => {
</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}
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} />
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -7,11 +7,6 @@ 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';
@ -19,7 +14,7 @@ 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';
@ -122,23 +117,13 @@ export function ParsingStatusCell({
)}
{isParserRunning(run) ? (
<>
<HoverCard>
<HoverCardTrigger asChild>
<div
className="flex items-center gap-1"
className="flex items-center gap-1 cursor-pointer"
onClick={() => handleShowLog(record)}
>
<Progress value={p} className="h-1 flex-1 min-w-10" />
{p}%
</div>
</HoverCardTrigger>
<HoverCardContent className="w-[40vw]">
<PopoverContent
record={record}
handleShowLog={handleShowLog}
></PopoverContent>
</HoverCardContent>
</HoverCard>
<div
className="cursor-pointer flex items-center gap-3"
onClick={

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import dayjs from 'dayjs';
import { toFixed } from './common-util';
export function formatDate(date: any) {
if (!date) {
@ -43,3 +44,20 @@ export function formatStandardDate(date: any) {
}
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('');
}