mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-04 01:25:07 +08:00
### What problem does this PR solve? Feat: Display the pipeline on the agent canvas #9869 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
137
web/src/pages/agent/pipeline-log-sheet/dataflow-timeline.tsx
Normal file
137
web/src/pages/agent/pipeline-log-sheet/dataflow-timeline.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import {
|
||||
Timeline,
|
||||
TimelineContent,
|
||||
TimelineHeader,
|
||||
TimelineIndicator,
|
||||
TimelineItem,
|
||||
TimelineSeparator,
|
||||
TimelineTitle,
|
||||
} from '@/components/originui/timeline';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { ITraceData } from '@/interfaces/database/agent';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { File } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { Operator } from '../constant';
|
||||
import OperatorIcon from '../operator-icon';
|
||||
import useGraphStore from '../store';
|
||||
|
||||
export type DataflowTimelineProps = {
|
||||
traceList?: ITraceData[];
|
||||
};
|
||||
|
||||
const END = 'END';
|
||||
|
||||
interface DataflowTrace {
|
||||
datetime: string;
|
||||
elapsed_time: number;
|
||||
message: string;
|
||||
progress: number;
|
||||
timestamp: number;
|
||||
}
|
||||
export function DataflowTimeline({ traceList }: DataflowTimelineProps) {
|
||||
const getNode = useGraphStore((state) => state.getNode);
|
||||
|
||||
const getNodeData = useCallback(
|
||||
(componentId: string) => {
|
||||
return getNode(componentId)?.data;
|
||||
},
|
||||
[getNode],
|
||||
);
|
||||
|
||||
const getNodeLabel = useCallback(
|
||||
(componentId: string) => {
|
||||
return getNodeData(componentId)?.label as Operator;
|
||||
},
|
||||
[getNodeData],
|
||||
);
|
||||
|
||||
return (
|
||||
<Timeline>
|
||||
{Array.isArray(traceList) &&
|
||||
traceList?.map((item, index) => {
|
||||
const traces = item.trace as DataflowTrace[];
|
||||
const nodeLabel = getNodeLabel(item.component_id);
|
||||
|
||||
const latest = traces[traces.length - 1];
|
||||
const progress = latest.progress * 100;
|
||||
|
||||
return (
|
||||
<TimelineItem
|
||||
key={item.component_id}
|
||||
step={index}
|
||||
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8 pb-6"
|
||||
>
|
||||
<TimelineHeader>
|
||||
<TimelineSeparator className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-7 bg-accent-primary" />
|
||||
<TimelineTitle className="">
|
||||
<TimelineContent
|
||||
className={cn(
|
||||
'text-foreground rounded-lg border px-4 py-3',
|
||||
)}
|
||||
>
|
||||
<section className="flex items-center justify-between mb-2">
|
||||
<span className="flex-1 truncate">
|
||||
{getNodeData(item.component_id)?.name || END}
|
||||
</span>
|
||||
<div className="flex-1 flex items-center gap-5">
|
||||
<Progress value={progress} className="h-1 flex-1" />
|
||||
<span className="text-accent-primary text-xs">
|
||||
{progress.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<div className="divide-y space-y-1">
|
||||
{traces
|
||||
.filter((x) => !isEmpty(x.message))
|
||||
.map((x, idx) => (
|
||||
<section
|
||||
key={idx}
|
||||
className="text-text-secondary text-xs space-x-2 py-2.5 !m-0"
|
||||
>
|
||||
<span>{x.datetime}</span>
|
||||
{item.component_id !== 'END' && (
|
||||
<span
|
||||
className={cn({
|
||||
'text-state-error':
|
||||
x.message.startsWith('[ERROR]'),
|
||||
})}
|
||||
>
|
||||
{x.message}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{x.elapsed_time.toString().slice(0, 6)}s
|
||||
</span>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</TimelineContent>
|
||||
</TimelineTitle>
|
||||
<TimelineIndicator
|
||||
className={cn(
|
||||
'border border-accent-primary group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-5 items-center justify-center group-data-[orientation=vertical]/timeline:-left-7',
|
||||
{
|
||||
'rounded bg-accent-primary': nodeLabel === Operator.Begin,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.component_id === END ? (
|
||||
<span className="rounded-full inline-block size-2 bg-accent-primary"></span>
|
||||
) : nodeLabel === Operator.Begin ? (
|
||||
<File className="size-3.5 text-bg-base"></File>
|
||||
) : (
|
||||
<OperatorIcon
|
||||
name={nodeLabel}
|
||||
className="size-3.5 rounded-full"
|
||||
></OperatorIcon>
|
||||
)}
|
||||
</TimelineIndicator>
|
||||
</TimelineHeader>
|
||||
</TimelineItem>
|
||||
);
|
||||
})}
|
||||
</Timeline>
|
||||
);
|
||||
}
|
||||
114
web/src/pages/agent/pipeline-log-sheet/index.tsx
Normal file
114
web/src/pages/agent/pipeline-log-sheet/index.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { SkeletonCard } from '@/components/skeleton-card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PipelineResultSearchParams } from '@/pages/dataflow-result/constant';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
CirclePause,
|
||||
Logs,
|
||||
SquareArrowOutUpRight,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import 'react18-json-view/src/style.css';
|
||||
import { useParams } from 'umi';
|
||||
import {
|
||||
isEndOutputEmpty,
|
||||
useDownloadOutput,
|
||||
} from '../hooks/use-download-output';
|
||||
import { UseFetchLogReturnType } from '../hooks/use-fetch-pipeline-log';
|
||||
import { DataflowTimeline } from './dataflow-timeline';
|
||||
|
||||
type LogSheetProps = IModalProps<any> & {
|
||||
handleCancel(): void;
|
||||
uploadedFileData?: Record<string, any>;
|
||||
} & Pick<
|
||||
UseFetchLogReturnType,
|
||||
'isCompleted' | 'isLogEmpty' | 'isParsing' | 'logs' | 'messageId'
|
||||
>;
|
||||
|
||||
export function PipelineLogSheet({
|
||||
hideModal,
|
||||
isParsing,
|
||||
logs,
|
||||
handleCancel,
|
||||
isCompleted,
|
||||
isLogEmpty,
|
||||
messageId,
|
||||
uploadedFileData,
|
||||
}: LogSheetProps) {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const { data: agent } = useFetchAgent();
|
||||
|
||||
const { handleDownloadJson } = useDownloadOutput(logs);
|
||||
const { navigateToDataflowResult } = useNavigatePage();
|
||||
|
||||
return (
|
||||
<Sheet open onOpenChange={hideModal} modal={false}>
|
||||
<SheetContent
|
||||
className={cn('top-20 h-auto flex flex-col p-0 gap-0')}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<SheetHeader className="p-5">
|
||||
<SheetTitle className="flex items-center gap-2.5">
|
||||
<Logs className="size-4" /> {t('flow.log')}
|
||||
{isCompleted && (
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
onClick={navigateToDataflowResult({
|
||||
id: messageId, // 'log_id',
|
||||
[PipelineResultSearchParams.AgentId]: id, // 'agent_id',
|
||||
[PipelineResultSearchParams.DocumentId]: uploadedFileData?.id, //'doc_id',
|
||||
[PipelineResultSearchParams.AgentTitle]: agent.title, //'title',
|
||||
[PipelineResultSearchParams.IsReadOnly]: 'true',
|
||||
[PipelineResultSearchParams.Type]: 'dataflow',
|
||||
[PipelineResultSearchParams.CreatedBy]:
|
||||
uploadedFileData?.created_by,
|
||||
[PipelineResultSearchParams.DocumentExtension]:
|
||||
uploadedFileData?.extension,
|
||||
})}
|
||||
>
|
||||
{t('dataflow.viewResult')} <ArrowUpRight />
|
||||
</Button>
|
||||
)}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<section className="flex-1 overflow-auto px-5 pt-5">
|
||||
{isLogEmpty ? (
|
||||
<SkeletonCard className="mt-2" />
|
||||
) : (
|
||||
<DataflowTimeline traceList={logs}></DataflowTimeline>
|
||||
)}
|
||||
</section>
|
||||
<div className="px-5 pb-5">
|
||||
{isParsing ? (
|
||||
<Button
|
||||
className="w-full mt-8 bg-state-error/10 text-state-error hover:bg-state-error hover:text-bg-base"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<CirclePause /> {t('dataflow.cancel')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleDownloadJson}
|
||||
disabled={isEndOutputEmpty(logs)}
|
||||
className="w-full mt-8 bg-accent-primary-5 text-text-secondary hover:bg-accent-primary-5 hover:text-accent-primary hover:border-accent-primary hover:border"
|
||||
>
|
||||
<SquareArrowOutUpRight />
|
||||
{t('dataflow.exportJson')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user