Feat: Cancel a running data flow test #9869 (#10257)

### What problem does this PR solve?

Feat: Cancel a running data flow test #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-09-24 16:33:33 +08:00
committed by GitHub
parent 5715ca6b74
commit 6bf0cda16f
17 changed files with 251 additions and 84 deletions

View File

@ -28,7 +28,7 @@ const DualRangeSlider = React.forwardRef<
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-border-button">
<SliderPrimitive.Range className="absolute h-full bg-accent-primary" />
</SliderPrimitive.Track>
{initialValue.map((value, index) => (

View File

@ -52,6 +52,7 @@ export const enum AgentApiAction {
FetchExternalAgentInputs = 'fetchExternalAgentInputs',
SetAgentSetting = 'setAgentSetting',
FetchPrompt = 'fetchPrompt',
CancelDataflow = 'cancelDataflow',
}
export const EmptyDsl = {
@ -387,7 +388,7 @@ export const useUploadCanvasFileWithProgress = (
files.forEach((file) => {
onError(file, error as Error);
});
message.error(error?.message);
message.error((error as Error)?.message || 'Upload failed');
}
},
});
@ -425,7 +426,7 @@ export const useFetchMessageTrace = (
},
});
return { data, loading, refetch, setMessageId };
return { data, loading, refetch, setMessageId, messageId };
};
export const useTestDbConnect = () => {
@ -571,7 +572,6 @@ export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => {
initialData: {} as IAgentLogsResponse,
gcTime: 0,
queryFn: async () => {
console.log('useFetchAgentLog', searchParams);
const { data } = await fetchAgentLogsByCanvasId(id as string, {
...searchParams,
});
@ -678,3 +678,24 @@ export const useFetchAgentList = ({
return { data, loading };
};
export const useCancelDataflow = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.CancelDataflow],
mutationFn: async (taskId: string) => {
const ret = await agentService.cancelDataflow(taskId);
if (ret?.data?.code === 0) {
message.success('success');
} else {
message.error(ret?.data?.data);
}
return ret?.data?.code;
},
});
return { data, loading, cancelDataflow: mutateAsync };
};

View File

@ -1579,6 +1579,7 @@ This delimiter is used to split the input text into several text pieces echo of
sqlStatementTip:
'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.',
frameworkPrompts: 'Framework',
release: 'Publish',
},
llmTools: {
bad_calculator: {
@ -1702,6 +1703,7 @@ This delimiter is used to split the input text into several text pieces echo of
begin: 'File',
parserMethod: 'Parser method',
exportJson: 'Export JSON',
viewResult: 'View Result',
},
},
};

View File

@ -1490,6 +1490,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
sqlStatementTip:
'在此处编写您的 SQL 查询。您可以使用变量、原始 SQL或使用变量语法混合使用两者。',
frameworkPrompts: '框架',
release: '发布',
},
footer: {
profile: 'All rights reserved @ React',
@ -1620,6 +1621,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
begin: '文件',
parserMethod: '解析方法',
exportJson: '导出 JSON',
viewResult: '查看结果',
},
},
};

View File

@ -35,7 +35,6 @@ import {
useHideFormSheetOnNodeDeletion,
useShowDrawer,
} from '../hooks/use-show-drawer';
import { LogSheet } from '../log-sheet';
import RunSheet from '../run-sheet';
import { ButtonEdge } from './edge';
import styles from './index.less';
@ -65,9 +64,10 @@ const edgeTypes = {
interface IProps {
drawerVisible: boolean;
hideDrawer(): void;
showLogSheet(): void;
}
function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
const { t } = useTranslation();
const {
nodes,
@ -147,17 +147,10 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
clearActiveDropdown,
]);
const {
visible: logSheetVisible,
showModal: showLogSheet,
hideModal: hideLogSheet,
} = useSetModalState();
const {
run,
loading: running,
messageId,
} = useRunDataflow(showLogSheet!, hideRunOrChatDrawer);
const { run, loading: running } = useRunDataflow(
showLogSheet!,
hideRunOrChatDrawer,
);
const onConnect = (connection: Connection) => {
originalOnConnect(connection);
@ -311,9 +304,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
loading={running}
></RunSheet>
)}
{logSheetVisible && (
<LogSheet hideModal={hideLogSheet} messageId={messageId}></LogSheet>
)}
{/* {logSheetVisible && <LogSheet hideModal={hideLogSheet}></LogSheet>} */}
</div>
);
}

View File

@ -48,3 +48,10 @@ export type HandleContextType = {
export const HandleContext = createContext<HandleContextType>(
{} as HandleContextType,
);
export type LogContextType = {
messageId: string;
setMessageId: (messageId: string) => void;
};
export const LogContext = createContext<LogContextType>({} as LogContextType);

View File

@ -0,0 +1,24 @@
import { useCancelDataflow } from '@/hooks/use-agent-request';
import { useCallback } from 'react';
export function useCancelCurrentDataflow({
messageId,
setMessageId,
hideLogSheet,
}: {
messageId: string;
setMessageId: (messageId: string) => void;
hideLogSheet(): void;
}) {
const { cancelDataflow } = useCancelDataflow();
const handleCancel = useCallback(async () => {
const code = await cancelDataflow(messageId);
if (code === 0) {
setMessageId('');
hideLogSheet();
}
}, [cancelDataflow, hideLogSheet, messageId, setMessageId]);
return { handleCancel };
}

View File

@ -0,0 +1,30 @@
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
export function useFetchLog() {
const { setMessageId, data, loading, messageId } =
useFetchMessageTrace(false);
const isCompleted = useMemo(() => {
if (Array.isArray(data)) {
const latest = data?.at(-1);
return (
latest?.component_id === 'END' && !isEmpty(latest?.trace[0].message)
);
}
return true;
}, [data]);
const isLogEmpty = !data || !data.length;
return {
data,
isLogEmpty,
isCompleted,
loading,
isParsing: !isLogEmpty && !isCompleted,
messageId,
setMessageId,
};
}

View File

@ -1,8 +1,9 @@
import { useSendMessageBySSE } from '@/hooks/use-send-message';
import api from '@/utils/api';
import { get } from 'lodash';
import { useCallback, useState } from 'react';
import { useCallback, useContext } from 'react';
import { useParams } from 'umi';
import { LogContext } from '../context';
import { useSaveGraphBeforeOpeningDebugDrawer } from './use-save-graph';
export function useRunDataflow(
@ -11,7 +12,7 @@ export function useRunDataflow(
) {
const { send } = useSendMessageBySSE(api.runCanvas);
const { id } = useParams();
const [messageId, setMessageId] = useState();
const { setMessageId } = useContext(LogContext);
const { handleRun: saveGraph, loading } =
useSaveGraphBeforeOpeningDebugDrawer(showLogSheet!);
@ -39,10 +40,10 @@ export function useRunDataflow(
return msgId;
}
},
[hideRunOrChatDrawer, id, saveGraph, send],
[hideRunOrChatDrawer, id, saveGraph, send, setMessageId],
);
return { run, loading: loading, messageId };
return { run, loading: loading };
}
export type RunDataflowType = ReturnType<typeof useRunDataflow>;

View File

@ -30,13 +30,17 @@ import { ComponentPropsWithoutRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import DataFlowCanvas from './canvas';
import { DropdownProvider } from './canvas/context';
import { LogContext } from './context';
import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow';
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useFetchLog } from './hooks/use-fetch-log';
import {
useSaveGraph,
useSaveGraphBeforeOpeningDebugDrawer,
useWatchAgentChange,
} from './hooks/use-save-graph';
import { LogSheet } from './log-sheet';
import { SettingDialog } from './setting-dialog';
import { useAgentHistoryManager } from './use-agent-history-manager';
import { VersionDialog } from './version-dialog';
@ -65,9 +69,7 @@ export default function DataFlow() {
const { saveGraph, loading } = useSaveGraph();
const { flowDetail: agentDetail } = useFetchDataOnMount();
const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
const handleRunAgent = useCallback(() => {
handleRun();
}, [handleRun]);
const {
visible: versionDialogVisible,
hideModal: hideVersionDialog,
@ -80,6 +82,29 @@ export default function DataFlow() {
showModal: showSettingDialog,
} = useSetModalState();
const {
visible: logSheetVisible,
showModal: showLogSheet,
hideModal: hideLogSheet,
} = useSetModalState();
const { isParsing, data, messageId, setMessageId } = useFetchLog();
const handleRunAgent = useCallback(() => {
if (isParsing) {
// show log sheet
showLogSheet();
} else {
handleRun();
}
}, [handleRun, isParsing, showLogSheet]);
const { handleCancel } = useCancelCurrentDataflow({
messageId,
setMessageId,
hideLogSheet,
});
const time = useWatchAgentChange(chatDrawerVisible);
return (
@ -112,14 +137,17 @@ export default function DataFlow() {
<LaptopMinimalCheck /> {t('flow.save')}
</ButtonLoading>
<Button variant={'secondary'} onClick={handleRunAgent}>
<CirclePlay />
{t('flow.run')}
<CirclePlay className={isParsing ? 'animate-spin' : ''} />
{isParsing ? 'running' : t('flow.run')}
</Button>
<Button variant={'secondary'} onClick={showVersionDialog}>
<History />
{t('flow.historyversion')}
</Button>
{/* <Button variant={'secondary'}>
<Send />
{t('flow.release')}
</Button> */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'secondary'}>
@ -140,15 +168,17 @@ export default function DataFlow() {
</DropdownMenu>
</div>
</PageHeader>
<ReactFlowProvider>
<DropdownProvider>
<DataFlowCanvas
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
></DataFlowCanvas>
</DropdownProvider>
</ReactFlowProvider>
<LogContext.Provider value={{ messageId, setMessageId }}>
<ReactFlowProvider>
<DropdownProvider>
<DataFlowCanvas
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
showLogSheet={showLogSheet}
></DataFlowCanvas>
</DropdownProvider>
</ReactFlowProvider>
</LogContext.Provider>
{versionDialogVisible && (
<DropdownProvider>
<VersionDialog hideModal={hideVersionDialog}></VersionDialog>
@ -157,6 +187,14 @@ export default function DataFlow() {
{settingDialogVisible && (
<SettingDialog hideModal={hideSettingDialog}></SettingDialog>
)}
{logSheetVisible && (
<LogSheet
hideModal={hideLogSheet}
isParsing={isParsing}
logs={data}
handleCancel={handleCancel}
></LogSheet>
)}
</section>
);
}

View File

@ -7,7 +7,10 @@ import {
TimelineSeparator,
TimelineTitle,
} from '@/components/originui/timeline';
import { Progress } from '@/components/ui/progress';
import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils';
import { File } from 'lucide-react';
import { useCallback } from 'react';
import { Operator } from '../constant';
import OperatorIcon from '../operator-icon';
@ -17,6 +20,8 @@ export type DataflowTimelineProps = {
traceList?: ITraceData[];
};
const END = 'END';
interface DataflowTrace {
datetime: string;
elapsed_time: number;
@ -48,43 +53,66 @@ export function DataflowTimeline({ traceList }: DataflowTimelineProps) {
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"
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="text-foreground mt-2 rounded-lg border px-4 py-3">
<p className="mb-2">
{getNodeData(item.component_id)?.name || 'END'}
</p>
<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}%
</span>
</div>
</section>
<div className="divide-y space-y-1">
{traces.map((x, idx) => (
<section
key={idx}
className="text-text-secondary text-xs"
className="text-text-secondary text-xs space-x-2 py-2.5 !m-0"
>
<div className="space-x-2">
<span>{x.datetime}</span>
<span>{x.progress * 100}%</span>
<span>{x.elapsed_time.toString().slice(0, 6)}</span>
</div>
<span>{x.datetime}</span>
{item.component_id !== 'END' && (
<div>{x.message}</div>
<span>{x.message}</span>
)}
<span>{x.elapsed_time.toString().slice(0, 6)}</span>
</section>
))}
</div>
</TimelineContent>
</TimelineTitle>
<TimelineIndicator className="border border-accent-primary group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-6 items-center justify-center group-data-[orientation=vertical]/timeline:-left-7">
{nodeLabel && (
<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-6 rounded-full"
className="size-3.5 rounded-full"
></OperatorIcon>
)}
</TimelineIndicator>

View File

@ -5,11 +5,15 @@ import {
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
import { IModalProps } from '@/interfaces/common';
import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils';
import { NotebookText, SquareArrowOutUpRight } from 'lucide-react';
import { useEffect } from 'react';
import {
ArrowUpRight,
CirclePause,
Logs,
SquareArrowOutUpRight,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import 'react18-json-view/src/style.css';
import {
@ -18,39 +22,53 @@ import {
} from '../hooks/use-download-output';
import { DataflowTimeline } from './dataflow-timeline';
type LogSheetProps = IModalProps<any> & { messageId?: string };
type LogSheetProps = IModalProps<any> & {
isParsing: boolean;
handleCancel(): void;
logs?: ITraceData[];
};
export function LogSheet({ hideModal, messageId }: LogSheetProps) {
export function LogSheet({
hideModal,
isParsing,
logs,
handleCancel,
}: LogSheetProps) {
const { t } = useTranslation();
const { setMessageId, data } = useFetchMessageTrace(false);
const { handleDownloadJson } = useDownloadOutput(data);
useEffect(() => {
if (messageId) {
setMessageId(messageId);
}
}, [messageId, setMessageId]);
const { handleDownloadJson } = useDownloadOutput(logs);
return (
<Sheet open onOpenChange={hideModal} modal={false}>
<SheetContent className={cn('top-20')}>
<SheetHeader>
<SheetTitle className="flex items-center gap-1">
<NotebookText className="size-4" /> {t('flow.log')}
<SheetTitle className="flex items-center gap-2.5">
<Logs className="size-4" /> {t('flow.log')}
<Button variant={'ghost'}>
{t('dataflow.viewResult')} <ArrowUpRight />
</Button>
</SheetTitle>
</SheetHeader>
<section className="max-h-[82vh] overflow-auto mt-6">
<DataflowTimeline traceList={data}></DataflowTimeline>
<DataflowTimeline traceList={logs}></DataflowTimeline>
</section>
<Button
onClick={handleDownloadJson}
disabled={isEndOutputEmpty(data)}
className="w-full mt-8"
>
<SquareArrowOutUpRight />
{t('dataflow.exportJson')}
</Button>
{isParsing ? (
<Button
className="w-full mt-8 bg-state-error/10 text-state-error"
onClick={handleCancel}
>
<CirclePause /> Cancel
</Button>
) : (
<Button
onClick={handleDownloadJson}
disabled={isEndOutputEmpty(logs)}
className="w-full mt-8"
>
<SquareArrowOutUpRight />
{t('dataflow.exportJson')}
</Button>
)}
</SheetContent>
</Sheet>
);

View File

@ -2,9 +2,9 @@ import { IconFont } from '@/components/icon-font';
import { cn } from '@/lib/utils';
import {
Blocks,
File,
FileChartColumnIncreasing,
Heading,
HousePlus,
ListMinus,
} from 'lucide-react';
import { Operator } from './constant';
@ -15,11 +15,11 @@ interface IProps {
}
export const OperatorIconMap = {
[Operator.Begin]: 'house-plus',
[Operator.Note]: 'notebook-pen',
};
export const SVGIconMap = {
[Operator.Begin]: File,
[Operator.Parser]: FileChartColumnIncreasing,
[Operator.Tokenizer]: ListMinus,
[Operator.Splitter]: Blocks,
@ -42,7 +42,7 @@ const OperatorIcon = ({ name, className }: IProps) => {
className,
)}
>
<HousePlus className="rounded size-3" />
<File className="rounded size-3" />
</div>
);
}
@ -50,7 +50,7 @@ const OperatorIcon = ({ name, className }: IProps) => {
return typeof Icon === 'string' ? (
<IconFont name={Icon} className={cn('size-5 ', className)}></IconFont>
) : (
<SvgIcon className="size-5"></SvgIcon>
<SvgIcon className={cn('size-5', className)}></SvgIcon>
);
};

View File

@ -29,6 +29,7 @@ const {
fetchAgentLogs,
fetchExternalAgentInputs,
prompt,
cancelDataflow,
} = api;
const methods = {
@ -120,6 +121,10 @@ const methods = {
url: prompt,
method: 'get',
},
cancelDataflow: {
url: cancelDataflow,
method: 'put',
},
} as const;
const agentService = registerNextServer<keyof typeof methods>(methods);

View File

@ -169,6 +169,7 @@ export default {
fetchExternalAgentInputs: (canvasId: string) =>
`${ExternalApi}${api_host}/agentbots/${canvasId}/inputs`,
prompt: `${api_host}/canvas/prompts`,
cancelDataflow: (id: string) => `${api_host}/canvas/cancel/${id}`,
// mcp server
listMcpServer: `${api_host}/mcp_server/list`,

View File

@ -28,7 +28,7 @@ module.exports = {
},
extend: {
colors: {
border: 'var(--colors-outline-neutral-strong)',
border: 'var(--border-default)',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'var(--background)',

View File

@ -100,7 +100,6 @@
--bg-card: rgba(0, 0, 0, 0.05);
--bg-component: #ffffff;
--bg-input: rgba(255, 255, 255, 0);
--bg-accent: rgba(76, 164, 231, 0.05);
/* Button ,Body text, Input completed text */
--text-primary: #161618;
--text-secondary: #75787a;