Feat: View data flow test results #9869 (#10392)

### What problem does this PR solve?

Feat: View data flow test results #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-09-30 18:55:55 +08:00
committed by GitHub
parent e87987785c
commit 7f62ab8eb3
16 changed files with 141 additions and 39 deletions

View File

@ -13,7 +13,9 @@ import {
} from './logic-hooks';
import { useGetKnowledgeSearchParams } from './route-hook';
export const useFetchNextChunkList = (): ResponseGetType<{
export const useFetchNextChunkList = (
enabled = true,
): ResponseGetType<{
data: IChunk[];
total: number;
documentInfo: IKnowledgeFile;
@ -37,6 +39,7 @@ export const useFetchNextChunkList = (): ResponseGetType<{
placeholderData: (previousData: any) =>
previousData ?? { data: [], total: 0, documentInfo: {} }, // https://github.com/TanStack/query/issues/8183
gcTime: 0,
enabled,
queryFn: async () => {
const { data } = await kbService.chunk_list({
doc_id: documentId,

View File

@ -1065,7 +1065,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
{input}
The above is the content you need to summarize.`,
createGraph: 'Create agent',
createFromTemplates: 'Create from templates',
createFromTemplates: 'Create from template',
retrieval: 'Retrieval',
generate: 'Generate',
answer: 'Interact',
@ -1586,9 +1586,12 @@ This delimiter is used to split the input text into several text pieces echo of
'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.',
frameworkPrompts: 'Framework',
release: 'Publish',
createFromBlank: 'Create from Blank',
createFromTemplate: 'Create from Template',
importJsonFile: 'Import json file',
createFromBlank: 'Create from blank',
createFromTemplate: 'Create from template',
importJsonFile: 'Import JSON file',
ceateAgent: 'Agent flow',
createPipeline: 'Data pipeline',
chooseAgentType: 'Choose Agent Type',
},
llmTools: {
bad_calculator: {

View File

@ -1500,6 +1500,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
createFromBlank: '从空白创建',
createFromTemplate: '从模板创建',
importJsonFile: '导入 JSON 文件',
chooseAgentType: '选择智能体类型',
},
footer: {
profile: 'All rights reserved @ React',

View File

@ -22,7 +22,7 @@ export function CreateAgentDialog({
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogContent>
<DialogHeader>
<DialogTitle>{t('flow.createGraph')}</DialogTitle>
</DialogHeader>

View File

@ -25,6 +25,7 @@ type FlowTypeCardProps = {
onChange?: (value: FlowType) => void;
};
function FlowTypeCards({ value, onChange }: FlowTypeCardProps) {
const { t } = useTranslation();
const handleChange = useCallback(
(value: FlowType) => () => {
onChange?.(value);
@ -59,7 +60,11 @@ function FlowTypeCards({ value, onChange }: FlowTypeCardProps) {
) : (
<Route className="size-6" />
)}
<p>{val}</p>
<p>
{t(
`flow.${val === FlowType.Agent ? 'createAgent' : 'createPipeline'}`,
)}
</p>
</div>
{isActive && <Check />}
</CardContent>
@ -106,7 +111,11 @@ export function CreateAgentForm({
id={TagRenameId}
>
{shouldChooseAgent && (
<RAGFlowFormItem required name="type" label={t('common.type')}>
<RAGFlowFormItem
required
name="type"
label={t('flow.chooseAgentType')}
>
<FlowTypeCards></FlowTypeCards>
</RAGFlowFormItem>
)}

View File

@ -16,12 +16,7 @@ export const NameFormSchema = {
export function NameFormField() {
const { t } = useTranslation();
return (
<RAGFlowFormItem
name="name"
required
label={t('common.name')}
tooltip={t('flow.sqlStatementTip')}
>
<RAGFlowFormItem name="name" required label={t('common.name')}>
<Input placeholder={t('common.namePlaceholder')} autoComplete="off" />
</RAGFlowFormItem>
);

View File

@ -52,6 +52,7 @@ export const HandleContext = createContext<HandleContextType>(
export type LogContextType = {
messageId: string;
setMessageId: (messageId: string) => void;
setUploadedFileData: (data: Record<string, any>) => void;
};
export const LogContext = createContext<LogContextType>({} as LogContextType);

View File

@ -13,7 +13,7 @@ export function useRunDataflow(
) {
const { send } = useSendMessageBySSE(api.runCanvas);
const { id } = useParams();
const { setMessageId } = useContext(LogContext);
const { setMessageId, setUploadedFileData } = useContext(LogContext);
const { handleRun: saveGraph, loading } =
useSaveGraphBeforeOpeningDebugDrawer(showLogSheet!);
@ -32,7 +32,7 @@ export function useRunDataflow(
if (res && res?.response.status === 200 && get(res, 'data.code') === 0) {
// fetch canvas
hideRunOrChatDrawer();
setUploadedFileData(fileResponseData.file);
const msgId = get(res, 'data.data.message_id');
if (msgId) {
setMessageId(msgId);
@ -43,7 +43,14 @@ export function useRunDataflow(
message.error(get(res, 'data.message', ''));
}
},
[hideRunOrChatDrawer, id, saveGraph, send, setMessageId],
[
hideRunOrChatDrawer,
id,
saveGraph,
send,
setMessageId,
setUploadedFileData,
],
);
return { run, loading: loading };

View File

@ -26,7 +26,7 @@ import {
Settings,
Upload,
} from 'lucide-react';
import { ComponentPropsWithoutRef, useCallback } from 'react';
import { ComponentPropsWithoutRef, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import DataFlowCanvas from './canvas';
import { DropdownProvider } from './canvas/context';
@ -99,6 +99,9 @@ export default function DataFlow() {
isLogEmpty,
} = useFetchLog(logSheetVisible);
const [uploadedFileData, setUploadedFileData] =
useState<Record<string, any>>();
const handleRunAgent = useCallback(() => {
if (isParsing) {
// show log sheet
@ -184,7 +187,9 @@ export default function DataFlow() {
</DropdownMenu>
</div>
</PageHeader>
<LogContext.Provider value={{ messageId, setMessageId }}>
<LogContext.Provider
value={{ messageId, setMessageId, setUploadedFileData }}
>
<ReactFlowProvider>
<DropdownProvider>
<DataFlowCanvas
@ -211,6 +216,8 @@ export default function DataFlow() {
isLogEmpty={isLogEmpty}
logs={logs}
handleCancel={handleCancel}
messageId={messageId}
uploadedFileData={uploadedFileData}
></LogSheet>
)}
</section>

View File

@ -7,6 +7,7 @@ import {
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';
@ -18,6 +19,7 @@ import {
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import 'react18-json-view/src/style.css';
import { useParams } from 'umi';
import {
isEndOutputEmpty,
useDownloadOutput,
@ -27,9 +29,10 @@ import { DataflowTimeline } from './dataflow-timeline';
type LogSheetProps = IModalProps<any> & {
handleCancel(): void;
uploadedFileData?: Record<string, any>;
} & Pick<
UseFetchLogReturnType,
'isCompleted' | 'isLogEmpty' | 'isParsing' | 'logs'
'isCompleted' | 'isLogEmpty' | 'isParsing' | 'logs' | 'messageId'
>;
export function LogSheet({
@ -39,11 +42,16 @@ export function LogSheet({
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
@ -57,14 +65,16 @@ export function LogSheet({
variant={'ghost'}
disabled={!isCompleted}
onClick={navigateToDataflowResult({
id: 'cfc28d6c9c4911f088bf047c16ec874f', // 'log_id',
[PipelineResultSearchParams.AgentId]:
'cfc28d6c9c4911f088bf047c16ec874f', // 'agent_id',
[PipelineResultSearchParams.DocumentId]:
'05b0e19a9d9d11f0b674047c16ec874f', //'doc_id',
[PipelineResultSearchParams.AgentTitle]: 'full', //'title',
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 />

View File

@ -1,8 +1,9 @@
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { api_host } from '@/utils/api';
import api, { api_host } from '@/utils/api';
import { useSize } from 'ahooks';
import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useGetPipelineResultSearchParams } from '../../hooks';
export const useDocumentResizeObserver = () => {
const [containerWidth, setContainerWidth] = useState<number>();
@ -44,12 +45,16 @@ export const useHighlightText = (searchText: string = '') => {
return textRenderer;
};
export const useGetDocumentUrl = () => {
export const useGetDocumentUrl = (isAgent: boolean) => {
const { documentId } = useGetKnowledgeSearchParams();
const { createdBy, documentId: id } = useGetPipelineResultSearchParams();
const url = useMemo(() => {
if (isAgent) {
return api.downloadFile + `?id=${id}&created_by=${createdBy}`;
}
return `${api_host}/document/get/${documentId}`;
}, [documentId]);
}, [createdBy, documentId, id, isAgent]);
return url;
};

View File

@ -20,4 +20,6 @@ export enum PipelineResultSearchParams {
IsReadOnly = 'is_read_only',
AgentId = 'agent_id',
AgentTitle = 'agent_title',
CreatedBy = 'created_by', // Who uploaded the file
DocumentExtension = 'extension',
}

View File

@ -3,6 +3,7 @@ import message from '@/components/ui/message';
import { useCreateChunk, useDeleteChunk } from '@/hooks/chunk-hooks';
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
import { IChunk } from '@/interfaces/database/knowledge';
import kbService from '@/services/knowledge-service';
import { formatSecondsToHumanReadable } from '@/utils/date';
@ -10,7 +11,7 @@ import { buildChunkHighlights } from '@/utils/document-util';
import { useMutation, useQuery } from '@tanstack/react-query';
import { t } from 'i18next';
import { camelCase, upperFirst } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { IHighlight } from 'react-pdf-highlighter';
import { useParams, useSearchParams } from 'umi';
import { ITimelineNodeObj, TimelineNodeObj } from './components/time-line';
@ -21,11 +22,15 @@ import {
} from './constant';
import { IDslComponent, IPipelineFileLogDetail } from './interface';
export const useFetchPipelineFileLogDetail = (props?: {
export const useFetchPipelineFileLogDetail = ({
isAgent = false,
isEdit = true,
refreshCount,
}: {
isEdit?: boolean;
refreshCount?: number;
isAgent: boolean;
}) => {
const { isEdit = true, refreshCount } = props || { isEdit: true };
const { id } = useParams();
const [searchParams] = useSearchParams();
const logId = searchParams.get('id') || id;
@ -39,6 +44,7 @@ export const useFetchPipelineFileLogDetail = (props?: {
queryKey,
initialData: {} as IPipelineFileLogDetail,
gcTime: 0,
enabled: !isAgent,
queryFn: async () => {
if (isEdit) {
const { data } = await kbService.get_pipeline_detail({
@ -287,5 +293,39 @@ export const useGetPipelineResultSearchParams = () => {
currentQueryParameters.get(PipelineResultSearchParams.AgentId) || '',
agentTitle:
currentQueryParameters.get(PipelineResultSearchParams.AgentTitle) || '',
documentExtension:
currentQueryParameters.get(
PipelineResultSearchParams.DocumentExtension,
) || '',
createdBy:
currentQueryParameters.get(PipelineResultSearchParams.CreatedBy) || '',
};
};
export function useFetchPipelineResult({
agentId,
}: Pick<ReturnType<typeof useGetPipelineResultSearchParams>, 'agentId'>) {
const [searchParams] = useSearchParams();
const messageId = searchParams.get('id');
const { data, setMessageId, setISStopFetchTrace } =
useFetchMessageTrace(agentId);
useEffect(() => {
if (messageId) {
setMessageId(messageId);
setISStopFetchTrace(true);
}
}, [agentId, messageId, setISStopFetchTrace, setMessageId]);
const pipelineResult = useMemo(() => {
if (Array.isArray(data)) {
const latest = data?.at(-1);
if (latest?.component_id === 'END' && Array.isArray(latest.trace)) {
return latest.trace.at(0);
}
}
}, [data]);
return { pipelineResult };
}

View File

@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import DocumentPreview from './components/document-preview';
import {
useFetchPipelineFileLogDetail,
useFetchPipelineResult,
useGetChunkHighlights,
useGetPipelineResultSearchParams,
useHandleChunkCardClick,
@ -26,28 +27,38 @@ import {
} from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import { Images } from '@/constants/common';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { useGetDocumentUrl } from './components/document-preview/hooks';
import TimelineDataFlow from './components/time-line';
import { TimelineNodeType } from './constant';
import styles from './index.less';
import { IDslComponent } from './interface';
import { IDslComponent, IPipelineFileLogDetail } from './interface';
import ParserContainer from './parser';
const Chunk = () => {
const { isReadOnly, knowledgeId, agentId, agentTitle } =
const { isReadOnly, knowledgeId, agentId, agentTitle, documentExtension } =
useGetPipelineResultSearchParams();
const isAgent = !!agentId;
const { pipelineResult } = useFetchPipelineResult({ agentId });
const {
data: { documentInfo },
} = useFetchNextChunkList();
} = useFetchNextChunkList(!isAgent);
const { selectedChunk, handleChunkCardClick } = useHandleChunkCardClick();
const [activeStepId, setActiveStepId] = useState<number | string>(2);
const { data: dataset } = useFetchPipelineFileLogDetail();
const { data: dataset } = useFetchPipelineFileLogDetail({
isAgent,
});
const { t } = useTranslation();
const { timelineNodes } = useTimelineDataFlow(dataset);
const { timelineNodes } = useTimelineDataFlow(
agentId ? (pipelineResult as IPipelineFileLogDetail) : dataset,
);
const {
navigateToDataset,
@ -55,12 +66,17 @@ const Chunk = () => {
navigateToAgents,
navigateToDataflow,
} = useNavigatePage();
const fileUrl = useGetDocumentUrl();
let fileUrl = useGetDocumentUrl(isAgent);
const { highlights, setWidthAndHeight } =
useGetChunkHighlights(selectedChunk);
const fileType = useMemo(() => {
if (isAgent) {
return Images.some((x) => x === documentExtension)
? 'visual'
: documentExtension;
}
switch (documentInfo?.type) {
case 'doc':
return documentInfo?.name.split('.').pop() || 'doc';
@ -72,7 +88,7 @@ const Chunk = () => {
return documentInfo?.type;
}
return 'unknown';
}, [documentInfo]);
}, [documentExtension, documentInfo?.name, documentInfo?.type, isAgent]);
const {
handleReRunFunc,

View File

@ -77,4 +77,6 @@ export interface NavigateToDataflowResultProps {
[PipelineResultSearchParams.AgentTitle]?: string;
[PipelineResultSearchParams.IsReadOnly]?: string;
[PipelineResultSearchParams.Type]: string;
[PipelineResultSearchParams.CreatedBy]: string;
[PipelineResultSearchParams.DocumentExtension]: string;
}

View File

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