Feat: Display the pipeline on the agent canvas #9869 (#10638)

### 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:
balibabu
2025-10-17 18:47:33 +08:00
committed by GitHub
parent c9e56d20cf
commit 685114d253
48 changed files with 2636 additions and 423 deletions

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

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