Feat: Displays the loading status of the data flow log #9869 (#10347)

### What problem does this PR solve?

Feat: Displays the loading status of the data flow log #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-09-28 19:38:46 +08:00
committed by GitHub
parent f4cc4dbd30
commit 664bc0b961
9 changed files with 121 additions and 56 deletions

View File

@ -416,13 +416,11 @@ export const useUploadCanvasFileWithProgress = (
return { data, loading, uploadCanvasFile: mutateAsync };
};
export const useFetchMessageTrace = (
isStopFetchTrace: boolean,
canvasId?: string,
) => {
export const useFetchMessageTrace = (canvasId?: string) => {
const { id } = useParams();
const queryId = id || canvasId;
const [messageId, setMessageId] = useState('');
const [isStopFetchTrace, setISStopFetchTrace] = useState(false);
const {
data,
@ -442,11 +440,19 @@ export const useFetchMessageTrace = (
message_id: messageId,
});
return data?.data ?? [];
return Array.isArray(data?.data) ? data?.data : [];
},
});
return { data, loading, refetch, setMessageId, messageId };
return {
data,
loading,
refetch,
setMessageId,
messageId,
isStopFetchTrace,
setISStopFetchTrace,
};
};
export const useTestDbConnect = () => {

View File

@ -1771,6 +1771,12 @@ Important structured information may include: names, dates, locations, events, k
cancel: 'Cancel',
swicthPromptMessage:
'The prompt word will change. Please confirm whether to abandon the existing prompt word?',
tokenizerFieldsOptions: {
text: 'Text',
keywords: 'Keywords',
questions: 'Questions',
summary: 'Augmented Context',
},
},
},
};

View File

@ -23,7 +23,7 @@ import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { get, isEmpty, isEqual, uniqWith } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import JsonView from 'react18-json-view';
import { Operator } from '../constant';
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
@ -116,12 +116,12 @@ export const WorkFlowTimeline = ({
isShare,
}: LogFlowTimelineProps) => {
// const getNode = useGraphStore((state) => state.getNode);
const [isStopFetchTrace, setISStopFetchTrace] = useState(false);
const { data: traceData, setMessageId } = useFetchMessageTrace(
isStopFetchTrace,
canvasId,
);
const {
data: traceData,
setMessageId,
setISStopFetchTrace,
} = useFetchMessageTrace(canvasId);
useEffect(() => {
setMessageId(currentMessageId);
@ -133,7 +133,7 @@ export const WorkFlowTimeline = ({
useEffect(() => {
setISStopFetchTrace(!sendLoading);
}, [sendLoading]);
}, [sendLoading, setISStopFetchTrace]);
const startedNodeList = useMemo(() => {
const finish = currentEventListWithoutMessage?.some(
@ -151,7 +151,7 @@ export const WorkFlowTimeline = ({
}
return pre;
}, []);
}, [currentEventListWithoutMessage, sendLoading]);
}, [currentEventListWithoutMessage, sendLoading, setISStopFetchTrace]);
const getElapsedTime = (nodeId: string) => {
if (nodeId === 'begin') {

View File

@ -31,12 +31,16 @@ export const FormSchema = z.object({
const SearchMethodOptions = buildOptions(TokenizerSearchMethod);
const FieldsOptions = buildOptions(TokenizerFields);
const TokenizerForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslation();
const defaultValues = useFormValues(initialTokenizerValues, node);
const FieldsOptions = buildOptions(
TokenizerFields,
t,
'dataflow.tokenizerFieldsOptions',
);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),

View File

@ -3,19 +3,19 @@ import { useCallback } from 'react';
export function useCancelCurrentDataflow({
messageId,
setMessageId,
stopFetchTrace,
}: {
messageId: string;
setMessageId: (messageId: string) => void;
stopFetchTrace(): void;
}) {
const { cancelDataflow } = useCancelDataflow();
const handleCancel = useCallback(async () => {
const code = await cancelDataflow(messageId);
if (code === 0) {
setMessageId('');
stopFetchTrace();
}
}, [cancelDataflow, messageId, setMessageId]);
}, [cancelDataflow, messageId, stopFetchTrace]);
return { handleCancel };
}

View File

@ -1,10 +1,16 @@
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
export function useFetchLog() {
const { setMessageId, data, loading, messageId } =
useFetchMessageTrace(false);
export function useFetchLog(logSheetVisible: boolean) {
const {
setMessageId,
data,
loading,
messageId,
setISStopFetchTrace,
isStopFetchTrace,
} = useFetchMessageTrace();
const isCompleted = useMemo(() => {
if (Array.isArray(data)) {
@ -13,18 +19,38 @@ export function useFetchLog() {
latest?.component_id === 'END' && !isEmpty(latest?.trace[0].message)
);
}
return true;
return false;
}, [data]);
const isLogEmpty = !data || !data.length;
const stopFetchTrace = useCallback(() => {
setISStopFetchTrace(true);
}, [setISStopFetchTrace]);
// cancel request
useEffect(() => {
if (isCompleted) {
stopFetchTrace();
}
}, [isCompleted, stopFetchTrace]);
useEffect(() => {
if (logSheetVisible) {
setISStopFetchTrace(false);
}
}, [logSheetVisible, setISStopFetchTrace]);
return {
data,
logs: data,
isLogEmpty,
isCompleted,
loading,
isParsing: !isLogEmpty && !isCompleted,
isParsing: !isLogEmpty && !isCompleted && !isStopFetchTrace,
messageId,
setMessageId,
stopFetchTrace,
};
}
export type UseFetchLogReturnType = ReturnType<typeof useFetchLog>;

View File

@ -89,20 +89,29 @@ export default function DataFlow() {
hideModal: hideLogSheet,
} = useSetModalState();
const { isParsing, data, messageId, setMessageId } = useFetchLog();
const {
isParsing,
logs,
messageId,
setMessageId,
isCompleted,
stopFetchTrace,
isLogEmpty,
} = useFetchLog(logSheetVisible);
const handleRunAgent = useCallback(() => {
if (isParsing) {
// show log sheet
showLogSheet();
} else {
hideLogSheet();
handleRun();
}
}, [handleRun, isParsing, showLogSheet]);
}, [handleRun, hideLogSheet, isParsing, showLogSheet]);
const { handleCancel } = useCancelCurrentDataflow({
messageId,
setMessageId,
stopFetchTrace,
});
const time = useWatchAgentChange(chatDrawerVisible);
@ -139,7 +148,6 @@ export default function DataFlow() {
<ButtonLoading
variant={'secondary'}
onClick={handleRunAgent}
disabled={isParsing}
loading={running}
>
{running || (
@ -199,7 +207,9 @@ export default function DataFlow() {
<LogSheet
hideModal={hideLogSheet}
isParsing={isParsing}
logs={data}
isCompleted={isCompleted}
isLogEmpty={isLogEmpty}
logs={logs}
handleCancel={handleCancel}
></LogSheet>
)}

View File

@ -10,6 +10,7 @@ import {
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';
@ -82,25 +83,29 @@ export function DataflowTimeline({ traceList }: DataflowTimelineProps) {
</div>
</section>
<div className="divide-y space-y-1">
{traces.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}
{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>
)}
<span>{x.elapsed_time.toString().slice(0, 6)}s</span>
</section>
))}
</section>
))}
</div>
</TimelineContent>
</TimelineTitle>

View File

@ -1,3 +1,4 @@
import { SkeletonCard } from '@/components/skeleton-card';
import { Button } from '@/components/ui/button';
import {
Sheet,
@ -6,7 +7,6 @@ import {
SheetTitle,
} from '@/components/ui/sheet';
import { IModalProps } from '@/interfaces/common';
import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils';
import {
ArrowUpRight,
@ -20,19 +20,23 @@ import {
isEndOutputEmpty,
useDownloadOutput,
} from '../hooks/use-download-output';
import { UseFetchLogReturnType } from '../hooks/use-fetch-log';
import { DataflowTimeline } from './dataflow-timeline';
type LogSheetProps = IModalProps<any> & {
isParsing: boolean;
handleCancel(): void;
logs?: ITraceData[];
};
} & Pick<
UseFetchLogReturnType,
'isCompleted' | 'isLogEmpty' | 'isParsing' | 'logs'
>;
export function LogSheet({
hideModal,
isParsing,
logs,
handleCancel,
isCompleted,
isLogEmpty,
}: LogSheetProps) {
const { t } = useTranslation();
@ -47,13 +51,17 @@ export function LogSheet({
<SheetHeader>
<SheetTitle className="flex items-center gap-2.5">
<Logs className="size-4" /> {t('flow.log')}
<Button variant={'ghost'}>
<Button variant={'ghost'} disabled={!isCompleted}>
{t('dataflow.viewResult')} <ArrowUpRight />
</Button>
</SheetTitle>
</SheetHeader>
<section className="max-h-[82vh] overflow-auto mt-6">
<DataflowTimeline traceList={logs}></DataflowTimeline>
{isLogEmpty ? (
<SkeletonCard className="mt-2" />
) : (
<DataflowTimeline traceList={logs}></DataflowTimeline>
)}
</section>
{isParsing ? (
<Button