Compare commits

...

4 Commits

Author SHA1 Message Date
a6039cf563 Fix: Optimized the timeline component and parser editing features #9869 (#10268)
### What problem does this PR solve?

Fix: Optimized the timeline component and parser editing features #9869

- Introduced the TimelineNodeType type, restructured the timeline node
structure, and supported dynamic node generation
- Enhanced the FormatPreserveEditor component to support editing and
line wrapping of JSON-formatted content
- Added a rerun function and loading state to the parser and splitter
components
- Adjusted the timeline style and interaction logic to enhance the user
experience
- Improved the modal component and added a destroy method to support
more flexible control
- Optimized the chunk result display and operation logic, supporting
batch deletion and selection
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-09-24 19:58:30 +08:00
8be7380b79 Feat: Added the context operator form for data flow #9869 (#10270)
### What problem does this PR solve?
Feat: Added the context operator form for data flow #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-09-24 19:58:16 +08:00
afb8a84f7b Feat: Add context node #9869 (#10266)
### What problem does this PR solve?

Feat: Add context node #9869
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-09-24 18:48:31 +08:00
6bf0cda16f 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)
2025-09-24 16:33:33 +08:00
53 changed files with 1243 additions and 529 deletions

View File

@ -1,6 +1,7 @@
'use client';
import { cn } from '@/lib/utils';
import { TimelineNodeType } from '@/pages/dataflow-result/constant';
import { parseColorToRGB } from '@/utils/common-util';
import { Slot } from '@radix-ui/react-slot';
import * as React from 'react';
@ -220,6 +221,8 @@ interface TimelineNode
completed?: boolean;
clickable?: boolean;
activeStyle?: TimelineIndicatorNodeProps;
detail?: any;
type?: TimelineNodeType;
}
interface CustomTimelineProps extends React.HTMLAttributes<HTMLDivElement> {
@ -252,7 +255,6 @@ const CustomTimeline = ({
const [internalActiveStep, setInternalActiveStep] =
React.useState(defaultValue);
const _lineColor = `rgb(${parseColorToRGB(lineColor)})`;
console.log(lineColor, _lineColor);
const currentActiveStep = activeStep ?? internalActiveStep;
const handleStepChange = (step: number, id: string | number) => {
@ -284,8 +286,6 @@ const CustomTimeline = ({
typeof _nodeSizeTemp === 'number'
? `${_nodeSizeTemp}px`
: _nodeSizeTemp;
console.log('icon-size', nodeSize, node.nodeSize, _nodeSize);
// const activeStyle = _activeStyle || {};
return (
<TimelineItem
@ -372,11 +372,10 @@ const CustomTimeline = ({
)}
</TimelineIndicator>
<TimelineHeader>
{node.date && <TimelineDate>{node.date}</TimelineDate>}
<TimelineHeader className="transform -translate-x-[40%] text-center">
<TimelineTitle
className={cn(
'text-sm font-medium',
'text-sm font-medium -ml-1',
isActive && _activeStyle.textColor
? `text-${_activeStyle.textColor}`
: '',
@ -387,6 +386,7 @@ const CustomTimeline = ({
>
{node.title}
</TimelineTitle>
{node.date && <TimelineDate>{node.date}</TimelineDate>}
</TimelineHeader>
{node.content && <TimelineContent>{node.content}</TimelineContent>}
</TimelineItem>

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

@ -31,6 +31,7 @@ export interface ModalProps {
export interface ModalType extends FC<ModalProps> {
show: typeof modalIns.show;
hide: typeof modalIns.hide;
destroy: typeof modalIns.destroy;
}
const Modal: ModalType = ({
@ -75,13 +76,13 @@ const Modal: ModalType = ({
const handleCancel = useCallback(() => {
onOpenChange?.(false);
// onCancel?.();
}, [onOpenChange]);
onCancel?.();
}, [onCancel, onOpenChange]);
const handleOk = useCallback(() => {
onOpenChange?.(true);
// onOk?.();
}, [onOpenChange]);
onOk?.();
}, [onOk, onOpenChange]);
const handleChange = (open: boolean) => {
onOpenChange?.(open);
console.log('open', open, onOpenChange);
@ -208,5 +209,6 @@ Modal.show = modalIns
return modalIns.show;
};
Modal.hide = modalIns.hide;
Modal.destroy = modalIns.destroy;
export { Modal };

View File

@ -1,3 +1,6 @@
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
import { ChatVariableEnabledField, variableEnabledFieldMap } from './chat';
export enum ProgrammingLanguage {
Python = 'python',
Javascript = 'javascript',
@ -26,3 +29,21 @@ export enum AgentGlobals {
}
export const AgentGlobalsSysQueryWithBrace = `{${AgentGlobals.SysQuery}}`;
export const variableCheckBoxFieldMap = Object.keys(
variableEnabledFieldMap,
).reduce<Record<string, boolean>>((pre, cur) => {
pre[cur] = setInitialChatVariableEnabledFieldValue(
cur as ChatVariableEnabledField,
);
return pre;
}, {});
export const initialLlmBaseValues = {
...variableCheckBoxFieldMap,
temperature: 0.1,
top_p: 0.3,
frequency_penalty: 0.7,
presence_penalty: 0.4,
max_tokens: 256,
};

View File

@ -133,10 +133,10 @@ export const useNavigatePage = () => {
);
const navigateToDataflowResult = useCallback(
(id: string, knowledgeId?: string) => () => {
(id: string, knowledgeId: string, doc_id?: string) => () => {
navigate(
// `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`,
`${Routes.DataflowResult}?id=${id}&type=dataflow`,
`${Routes.DataflowResult}?id=${id}&doc_id=${doc_id}&${QueryStringMap.KnowledgeId}=${knowledgeId}&type=dataflow`,
);
},
[navigate],

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,15 @@ 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',
running: 'Running',
context: 'Context Generator',
contextDescription: 'Context Generator',
summary: 'Summary',
keywords: 'Keywords',
questions: 'Questions',
metadata: 'Metadata',
fieldName: 'Result Destination',
},
},
};

View File

@ -1490,6 +1490,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
sqlStatementTip:
'在此处编写您的 SQL 查询。您可以使用变量、原始 SQL或使用变量语法混合使用两者。',
frameworkPrompts: '框架',
release: '发布',
},
footer: {
profile: 'All rights reserved @ React',
@ -1620,6 +1621,15 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
begin: '文件',
parserMethod: '解析方法',
exportJson: '导出 JSON',
viewResult: '查看结果',
running: '运行中',
context: '上下文生成器',
contextDescription: '上下文生成器',
summary: '摘要',
keywords: '关键词',
questions: '问题',
metadata: '元数据',
fieldName: '结果目的地',
},
},
};

View File

@ -7,6 +7,7 @@ import {
AgentGlobalsSysQueryWithBrace,
CodeTemplateStrMap,
ProgrammingLanguage,
initialLlmBaseValues,
} from '@/constants/agent';
export enum AgentDialogueMode {
@ -14,13 +15,8 @@ export enum AgentDialogueMode {
Task = 'task',
}
import {
ChatVariableEnabledField,
variableEnabledFieldMap,
} from '@/constants/chat';
import { ModelVariableType } from '@/constants/knowledge';
import i18n from '@/locales/config';
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
import { t } from 'i18next';
// DuckDuckGo's channel options
@ -271,24 +267,6 @@ export const initialBeginValues = {
prologue: `Hi! I'm your assistant. What can I do for you?`,
};
export const variableCheckBoxFieldMap = Object.keys(
variableEnabledFieldMap,
).reduce<Record<string, boolean>>((pre, cur) => {
pre[cur] = setInitialChatVariableEnabledFieldValue(
cur as ChatVariableEnabledField,
);
return pre;
}, {});
const initialLlmBaseValues = {
...variableCheckBoxFieldMap,
temperature: 0.1,
top_p: 0.3,
frequency_penalty: 0.7,
presence_penalty: 0.4,
max_tokens: 256,
};
export const initialGenerateValues = {
...initialLlmBaseValues,
prompt: i18n.t('flow.promptText'),

View File

@ -35,12 +35,12 @@ 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';
import { RagNode } from './node';
import { BeginNode } from './node/begin-node';
import { ContextNode } from './node/context-node';
import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown';
import { HierarchicalMergerNode } from './node/hierarchical-merger-node';
import NoteNode from './node/note-node';
@ -56,6 +56,7 @@ export const nodeTypes: NodeTypes = {
tokenizerNode: TokenizerNode,
splitterNode: SplitterNode,
hierarchicalMergerNode: HierarchicalMergerNode,
contextNode: ContextNode,
};
const edgeTypes = {
@ -65,9 +66,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 +149,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 +306,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
loading={running}
></RunSheet>
)}
{logSheetVisible && (
<LogSheet hideModal={hideLogSheet} messageId={messageId}></LogSheet>
)}
{/* {logSheetVisible && <LogSheet hideModal={hideLogSheet}></LogSheet>} */}
</div>
);
}

View File

@ -0,0 +1 @@
export { RagNode as ContextNode } from './index';

View File

@ -124,6 +124,7 @@ function AccordionOperators({
Operator.Tokenizer,
Operator.Splitter,
Operator.HierarchicalMerger,
Operator.Context,
]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}

View File

@ -2,7 +2,6 @@ import { IRagNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react';
import { memo } from 'react';
import { NodeHandleId } from '../../constant';
import { needsSingleStepDebugging } from '../../utils';
import { CommonHandle } from './handle';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
@ -16,12 +15,7 @@ function InnerRagNode({
selected,
}: NodeProps<IRagNode>) {
return (
<ToolBar
selected={selected}
id={id}
label={data.label}
showRun={needsSingleStepDebugging(data.label)}
>
<ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper selected={selected}>
<CommonHandle
id={NodeHandleId.End}

View File

@ -34,7 +34,7 @@ export function ToolBar({
children,
label,
id,
showRun = true,
showRun = false,
}: ToolBarProps) {
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);

View File

@ -1,3 +1,5 @@
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
import { initialLlmBaseValues } from '@/constants/agent';
import {
ChatVariableEnabledField,
variableEnabledFieldMap,
@ -15,6 +17,89 @@ import {
WrapText,
} from 'lucide-react';
export enum FileType {
PDF = 'pdf',
Spreadsheet = 'spreadsheet',
Image = 'image',
Email = 'email',
TextMarkdown = 'text&markdown',
Docx = 'word',
PowerPoint = 'slides',
Video = 'video',
Audio = 'audio',
}
export enum PdfOutputFormat {
Json = 'json',
Markdown = 'markdown',
}
export enum SpreadsheetOutputFormat {
Json = 'json',
Html = 'html',
}
export enum ImageOutputFormat {
Text = 'text',
}
export enum EmailOutputFormat {
Json = 'json',
Text = 'text',
}
export enum TextMarkdownOutputFormat {
Text = 'text',
}
export enum DocxOutputFormat {
Markdown = 'markdown',
Json = 'json',
}
export enum PptOutputFormat {
Json = 'json',
}
export enum VideoOutputFormat {
Json = 'json',
}
export enum AudioOutputFormat {
Text = 'text',
}
export const OutputFormatMap = {
[FileType.PDF]: PdfOutputFormat,
[FileType.Spreadsheet]: SpreadsheetOutputFormat,
[FileType.Image]: ImageOutputFormat,
[FileType.Email]: EmailOutputFormat,
[FileType.TextMarkdown]: TextMarkdownOutputFormat,
[FileType.Docx]: DocxOutputFormat,
[FileType.PowerPoint]: PptOutputFormat,
[FileType.Video]: VideoOutputFormat,
[FileType.Audio]: AudioOutputFormat,
};
export const InitialOutputFormatMap = {
[FileType.PDF]: PdfOutputFormat.Json,
[FileType.Spreadsheet]: SpreadsheetOutputFormat.Html,
[FileType.Image]: ImageOutputFormat.Text,
[FileType.Email]: EmailOutputFormat.Text,
[FileType.TextMarkdown]: TextMarkdownOutputFormat.Text,
[FileType.Docx]: DocxOutputFormat.Json,
[FileType.PowerPoint]: PptOutputFormat.Json,
[FileType.Video]: VideoOutputFormat.Json,
[FileType.Audio]: AudioOutputFormat.Text,
};
export enum ContextGeneratorFieldName {
Summary = 'summary',
Keywords = 'keywords',
Questions = 'questions',
Metadata = 'metadata',
}
export enum PromptRole {
User = 'user',
Assistant = 'assistant',
@ -34,6 +119,7 @@ export enum Operator {
Tokenizer = 'Tokenizer',
Splitter = 'Splitter',
HierarchicalMerger = 'HierarchicalMerger',
Context = 'Context',
}
export const SwitchLogicOperatorOptions = ['and', 'or'];
@ -76,9 +162,34 @@ export enum ImageParseMethod {
OCR = 'ocr',
}
export enum TokenizerFields {
Text = 'text',
Questions = 'questions',
Summary = 'summary',
}
export enum ParserFields {
From = 'from',
To = 'to',
Cc = 'cc',
Bcc = 'bcc',
Date = 'date',
Subject = 'subject',
Body = 'body',
Attachments = 'attachments',
}
export const initialBeginValues = {
mode: AgentDialogueMode.Conversational,
prologue: `Hi! I'm your assistant. What can I do for you?`,
outputs: {
name: {
type: 'string',
value: '',
},
file: {
type: 'Object',
value: {},
},
},
};
export const variableCheckBoxFieldMap = Object.keys(
@ -100,7 +211,7 @@ export const initialTokenizerValues = {
TokenizerSearchMethod.FullText,
],
filename_embd_weight: 0.1,
fields: ['text'],
fields: TokenizerFields.Text,
outputs: {},
};
@ -118,10 +229,40 @@ export enum StringTransformDelimiter {
Space = ' ',
}
export const initialParserValues = { outputs: {}, setups: [] };
export const initialParserValues = {
outputs: {
markdown: { type: 'string', value: '' },
text: { type: 'string', value: '' },
html: { type: 'string', value: '' },
json: { type: 'Array<object>', value: [] },
},
setups: [
{
fileFormat: FileType.PDF,
output_format: PdfOutputFormat.Json,
parse_method: ParseDocumentType.DeepDOC,
},
{
fileFormat: FileType.Spreadsheet,
output_format: SpreadsheetOutputFormat.Html,
},
{
fileFormat: FileType.Image,
output_format: ImageOutputFormat.Text,
parse_method: ImageParseMethod.OCR,
},
{
fileFormat: FileType.Email,
fields: Object.values(ParserFields),
output_format: EmailOutputFormat.Text,
},
],
};
export const initialSplitterValues = {
outputs: {},
outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
chunk_token_size: 512,
overlapped_percent: 0,
delimiters: [{ value: '\n' }],
@ -136,7 +277,9 @@ export enum Hierarchy {
}
export const initialHierarchicalMergerValues = {
outputs: {},
outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
hierarchy: Hierarchy.H3,
levels: [
{ expressions: [{ expression: '^#[^#]' }] },
@ -146,6 +289,12 @@ export const initialHierarchicalMergerValues = {
],
};
export const initialContextValues = {
...initialLlmBaseValues,
field_name: [ContextGeneratorFieldName.Summary],
outputs: {},
};
export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 },
{ top: 8, right: 18 },
@ -178,6 +327,7 @@ export const NodeMap = {
[Operator.Tokenizer]: 'tokenizerNode',
[Operator.Splitter]: 'splitterNode',
[Operator.HierarchicalMerger]: 'hierarchicalMergerNode',
[Operator.Context]: 'contextNode',
};
export enum BeginQueryType {
@ -220,18 +370,6 @@ export enum AgentExceptionMethod {
Goto = 'goto',
}
export enum FileType {
PDF = 'pdf',
Spreadsheet = 'spreadsheet',
Image = 'image',
Email = 'email',
TextMarkdown = 'text&markdown',
Docx = 'word',
PowerPoint = 'slides',
Video = 'video',
Audio = 'audio',
}
export const FileTypeSuffixMap = {
[FileType.PDF]: ['pdf'],
[FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'],

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

@ -1,4 +1,5 @@
import { Operator } from '../constant';
import ContextForm from '../form/context-form';
import HierarchicalMergerForm from '../form/hierarchical-merger-form';
import ParserForm from '../form/parser-form';
import SplitterForm from '../form/splitter-form';
@ -23,4 +24,7 @@ export const FormConfigMap = {
[Operator.HierarchicalMerger]: {
component: HierarchicalMergerForm,
},
[Operator.Context]: {
component: ContextForm,
},
};

View File

@ -0,0 +1,86 @@
import { LargeModelFormField } from '@/components/large-model-form-field';
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Form } from '@/components/ui/form';
import { MultiSelect } from '@/components/ui/multi-select';
import { useBuildPromptExtraPromptOptions } from '@/pages/agent/form/agent-form/use-build-prompt-options';
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
ContextGeneratorFieldName,
initialContextValues,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import useGraphStore from '../../store';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
const outputList = buildOutputList(initialContextValues.outputs);
export const FormSchema = z.object({
sys_prompt: z.string(),
prompts: z.string().optional(),
...LlmSettingSchema,
field_name: z.array(z.string()),
});
export type ContextFormSchemaType = z.infer<typeof FormSchema>;
const ContextForm = ({ node }: INextOperatorForm) => {
const defaultValues = useFormValues(initialContextValues, node);
const { t } = useTranslation();
const form = useForm<ContextFormSchemaType>({
defaultValues,
resolver: zodResolver(FormSchema),
});
const { edges } = useGraphStore((state) => state);
const { extraOptions } = useBuildPromptExtraPromptOptions(edges, node?.id);
const options = buildOptions(ContextGeneratorFieldName, t, 'dataflow');
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<LargeModelFormField></LargeModelFormField>
<RAGFlowFormItem label={t('flow.systemPrompt')} name="sys_prompt">
<PromptEditor
placeholder={t('flow.messagePlaceholder')}
showToolbar={true}
extraOptions={extraOptions}
></PromptEditor>
</RAGFlowFormItem>
<RAGFlowFormItem label={t('flow.userPrompt')} name="prompts">
<PromptEditor showToolbar={true}></PromptEditor>
</RAGFlowFormItem>
<RAGFlowFormItem label={t('dataflow.fieldName')} name="field_name">
{(field) => (
<MultiSelect
onValueChange={field.onChange}
placeholder={t('dataFlowPlaceholder')}
defaultValue={field.value}
options={options}
></MultiSelect>
)}
</RAGFlowFormItem>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
};
export default memo(ContextForm);

View File

@ -8,8 +8,7 @@ import {
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { buildOptions } from '@/utils/form';
import { useTranslation } from 'react-i18next';
import { FileType } from '../../constant';
import { OutputFormatMap } from './constant';
import { FileType, OutputFormatMap } from '../../constant';
import { CommonProps } from './interface';
import { buildFieldNameWithPrefix } from './utils';

View File

@ -1,65 +0,0 @@
import { FileType } from '../../constant';
export enum PdfOutputFormat {
Json = 'json',
Markdown = 'markdown',
}
export enum SpreadsheetOutputFormat {
Json = 'json',
Html = 'html',
}
export enum ImageOutputFormat {
Text = 'text',
}
export enum EmailOutputFormat {
Json = 'json',
Text = 'text',
}
export enum TextMarkdownOutputFormat {
Text = 'text',
}
export enum DocxOutputFormat {
Markdown = 'markdown',
Json = 'json',
}
export enum PptOutputFormat {
Json = 'json',
}
export enum VideoOutputFormat {
Json = 'json',
}
export enum AudioOutputFormat {
Text = 'text',
}
export const OutputFormatMap = {
[FileType.PDF]: PdfOutputFormat,
[FileType.Spreadsheet]: SpreadsheetOutputFormat,
[FileType.Image]: ImageOutputFormat,
[FileType.Email]: EmailOutputFormat,
[FileType.TextMarkdown]: TextMarkdownOutputFormat,
[FileType.Docx]: DocxOutputFormat,
[FileType.PowerPoint]: PptOutputFormat,
[FileType.Video]: VideoOutputFormat,
[FileType.Audio]: AudioOutputFormat,
};
export const InitialOutputFormatMap = {
[FileType.PDF]: PdfOutputFormat.Json,
[FileType.Spreadsheet]: SpreadsheetOutputFormat.Html,
[FileType.Image]: ImageOutputFormat.Text,
[FileType.Email]: EmailOutputFormat.Text,
[FileType.TextMarkdown]: TextMarkdownOutputFormat.Text,
[FileType.Docx]: DocxOutputFormat.Json,
[FileType.PowerPoint]: PptOutputFormat.Json,
[FileType.Video]: VideoOutputFormat.Json,
[FileType.Audio]: AudioOutputFormat.Text,
};

View File

@ -1,20 +1,12 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { MultiSelect } from '@/components/ui/multi-select';
import { buildOptions } from '@/utils/form';
import { useTranslation } from 'react-i18next';
import { ParserFields } from '../../constant';
import { CommonProps } from './interface';
import { buildFieldNameWithPrefix } from './utils';
const options = buildOptions([
'from',
'to',
'cc',
'bcc',
'date',
'subject',
'body',
'attachments',
]);
const options = buildOptions(ParserFields);
export function EmailFormFields({ prefix }: CommonProps) {
const { t } = useTranslation();
@ -24,7 +16,14 @@ export function EmailFormFields({ prefix }: CommonProps) {
name={buildFieldNameWithPrefix(`fields`, prefix)}
label={t('dataflow.fields')}
>
<SelectWithSearch options={options}></SelectWithSearch>
{(field) => (
<MultiSelect
options={options}
onValueChange={field.onChange}
defaultValue={field.value}
variant="inverted"
></MultiSelect>
)}
</RAGFlowFormItem>
</>
);

View File

@ -17,14 +17,17 @@ import {
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { FileType, initialParserValues } from '../../constant';
import {
FileType,
InitialOutputFormatMap,
initialParserValues,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { Output } from '../components/output';
import { OutputFormatFormField } from './common-form-fields';
import { InitialOutputFormatMap } from './constant';
import { EmailFormFields } from './email-form-fields';
import { ImageFormFields } from './image-form-fields';
import { PdfFormFields } from './pdf-form-fields';

View File

@ -1,3 +1,4 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { SliderInputFormField } from '@/components/slider-input-form-field';
import { Form } from '@/components/ui/form';
@ -8,7 +9,11 @@ import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { initialTokenizerValues, TokenizerSearchMethod } from '../../constant';
import {
initialTokenizerValues,
TokenizerFields,
TokenizerSearchMethod,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
@ -21,11 +26,12 @@ const outputList = buildOutputList(initialTokenizerValues.outputs);
export const FormSchema = z.object({
search_method: z.array(z.string()).min(1),
filename_embd_weight: z.number(),
fields: z.string(),
});
const SearchMethodOptions = buildOptions(TokenizerSearchMethod);
const FieldsOptions = [{ label: 'text', value: 'text' }];
const FieldsOptions = buildOptions(TokenizerFields);
const TokenizerForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslation();
@ -62,14 +68,7 @@ const TokenizerForm = ({ node }: INextOperatorForm) => {
step={0.01}
></SliderInputFormField>
<RAGFlowFormItem name="fields" label={t('dataflow.fields')}>
{(field) => (
<MultiSelect
options={FieldsOptions}
onValueChange={field.onChange}
defaultValue={field.value}
variant="inverted"
/>
)}
{(field) => <SelectWithSearch options={FieldsOptions} {...field} />}
</RAGFlowFormItem>
</FormWrapper>
<div className="p-5">

View File

@ -1,3 +1,4 @@
import { useFetchModelId } from '@/hooks/logic-hooks';
import { Connection, Node, Position, ReactFlowInstance } from '@xyflow/react';
import humanId from 'human-id';
import { lowerFirst } from 'lodash';
@ -8,6 +9,7 @@ import {
NodeMap,
Operator,
initialBeginValues,
initialContextValues,
initialHierarchicalMergerValues,
initialNoteValues,
initialParserValues,
@ -21,6 +23,8 @@ import {
} from '../utils';
export const useInitializeOperatorParams = () => {
const llmId = useFetchModelId();
const initialFormValuesMap = useMemo(() => {
return {
[Operator.Begin]: initialBeginValues,
@ -29,8 +33,9 @@ export const useInitializeOperatorParams = () => {
[Operator.Tokenizer]: initialTokenizerValues,
[Operator.Splitter]: initialSplitterValues,
[Operator.HierarchicalMerger]: initialHierarchicalMergerValues,
[Operator.Context]: { ...initialContextValues, llm_id: llmId },
};
}, []);
}, [llmId]);
const initializeOperatorParams = useCallback(
(operatorName: Operator) => {

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 (
@ -111,15 +136,22 @@ export default function DataFlow() {
>
<LaptopMinimalCheck /> {t('flow.save')}
</ButtonLoading>
<Button variant={'secondary'} onClick={handleRunAgent}>
<CirclePlay />
{t('flow.run')}
<Button
variant={'secondary'}
onClick={handleRunAgent}
disabled={isParsing}
>
<CirclePlay className={isParsing ? 'animate-spin' : ''} />
{isParsing ? t('dataflow.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 +172,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 +191,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,73 @@ 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
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="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,56 @@ 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')}>
<SheetContent
className={cn('top-20')}
onInteractOutside={(e) => e.preventDefault()}
>
<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,10 @@ import { IconFont } from '@/components/icon-font';
import { cn } from '@/lib/utils';
import {
Blocks,
File,
FileChartColumnIncreasing,
FileStack,
Heading,
HousePlus,
ListMinus,
} from 'lucide-react';
import { Operator } from './constant';
@ -15,15 +16,16 @@ 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,
[Operator.HierarchicalMerger]: Heading,
[Operator.Context]: FileStack,
};
const Empty = () => {
@ -42,7 +44,7 @@ const OperatorIcon = ({ name, className }: IProps) => {
className,
)}
>
<HousePlus className="rounded size-3" />
<File className="rounded size-3" />
</div>
);
}
@ -50,7 +52,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

@ -1,7 +1,6 @@
import { IAgentForm } from '@/interfaces/database/agent';
import { DSLComponents, RAGFlowNodeType } from '@/interfaces/database/flow';
import { removeUselessFieldsFromValues } from '@/utils/form';
import { Edge, Node, XYPosition } from '@xyflow/react';
import { Edge, XYPosition } from '@xyflow/react';
import { FormInstance, FormListFieldData } from 'antd';
import { humanId } from 'human-id';
import { curry, get, intersectionWith, isEmpty, isEqual, sample } from 'lodash';
@ -24,24 +23,13 @@ const buildComponentDownstreamOrUpstream = (
edges: Edge[],
nodeId: string,
isBuildDownstream = true,
nodes: Node[],
) => {
return edges
.filter((y) => {
const node = nodes.find((x) => x.id === nodeId);
let isNotUpstreamTool = true;
let isNotUpstreamAgent = true;
let isNotExceptionGoto = true;
if (isBuildDownstream && node?.data.label === Operator.Agent) {
isNotExceptionGoto = y.sourceHandle !== NodeHandleId.AgentException;
// Exclude the tool operator downstream of the agent operator
isNotUpstreamTool = !y.target.startsWith(Operator.Tool);
// Exclude the agent operator downstream of the agent operator
isNotUpstreamAgent = !(
y.target.startsWith(Operator.Agent) &&
y.targetHandle === NodeHandleId.AgentTop
);
}
return (
y[isBuildDownstream ? 'source' : 'target'] === nodeId &&
isNotUpstreamTool &&
@ -54,9 +42,9 @@ const buildComponentDownstreamOrUpstream = (
const removeUselessDataInTheOperator = curry(
(operatorName: string, params: Record<string, unknown>) => {
if (operatorName === Operator.Categorize) {
return removeUselessFieldsFromValues(params, '');
}
// if (operatorName === Operator.Categorize) {
// return removeUselessFieldsFromValues(params, '');
// }
return params;
},
);
@ -197,8 +185,8 @@ export const buildDslComponentsByGraph = (
component_name: operatorName,
params: buildOperatorParams(operatorName)(params) ?? {},
},
downstream: buildComponentDownstreamOrUpstream(edges, id, true, nodes),
upstream: buildComponentDownstreamOrUpstream(edges, id, false, nodes),
downstream: buildComponentDownstreamOrUpstream(edges, id, true),
upstream: buildComponentDownstreamOrUpstream(edges, id, false),
parent_id: x?.parentId,
};
});
@ -352,25 +340,6 @@ export const generateNodeNamesWithIncreasingIndex = (
export const duplicateNodeForm = (nodeData?: RAGFlowNodeType['data']) => {
const form: Record<string, any> = { ...(nodeData?.form ?? {}) };
// Delete the downstream node corresponding to the to field of the Categorize operator
if (nodeData?.label === Operator.Categorize) {
form.category_description = Object.keys(form.category_description).reduce<
Record<string, Record<string, any>>
>((pre, cur) => {
pre[cur] = {
...form.category_description[cur],
to: undefined,
};
return pre;
}, {});
}
// Delete the downstream nodes corresponding to the yes and no fields of the Relevant operator
if (nodeData?.label === Operator.Relevant) {
form.yes = undefined;
form.no = undefined;
}
return {
...(nodeData ?? { label: '' }),
form,

View File

@ -154,12 +154,7 @@ const ChunkerContainer = (props: IProps) => {
<RerunButton step={step} onRerun={handleReRunFunc} />
</div>
)}
<div
className={classNames(
{ [styles.pagePdfWrapper]: isPdf },
'flex flex-col w-full',
)}
>
<div className={classNames('flex flex-col w-full')}>
<Spin spinning={loading} className={styles.spin} size="large">
<div className="h-[50px] flex flex-row justify-between items-end pb-[5px]">
<div>

View File

@ -1,24 +1,17 @@
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Ban, CircleCheck, Trash2 } from 'lucide-react';
import { Trash2 } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type ICheckboxSetProps = {
selectAllChunk: (e: any) => void;
removeChunk: (e?: any) => void;
switchChunk: (available: number) => void;
checked: boolean;
selectedChunkIds: string[];
};
export default (props: ICheckboxSetProps) => {
const {
selectAllChunk,
removeChunk,
switchChunk,
checked,
selectedChunkIds,
} = props;
const { selectAllChunk, removeChunk, checked, selectedChunkIds } = props;
const { t } = useTranslation();
const handleSelectAllCheck = useCallback(
(e: any) => {
@ -32,14 +25,6 @@ export default (props: ICheckboxSetProps) => {
removeChunk();
}, [removeChunk]);
const handleEnabledClick = useCallback(() => {
switchChunk(1);
}, [switchChunk]);
const handleDisabledClick = useCallback(() => {
switchChunk(0);
}, [switchChunk]);
const isSelected = useMemo(() => {
return selectedChunkIds?.length > 0;
}, [selectedChunkIds]);
@ -57,20 +42,6 @@ export default (props: ICheckboxSetProps) => {
</div>
{isSelected && (
<>
<div
className="flex items-center cursor-pointer text-muted-foreground hover:text-text-primary"
onClick={handleEnabledClick}
>
<CircleCheck size={16} />
<span className="block ml-1">{t('chunk.enable')}</span>
</div>
<div
className="flex items-center cursor-pointer text-muted-foreground hover:text-text-primary"
onClick={handleDisabledClick}
>
<Ban size={16} />
<span className="block ml-1">{t('chunk.disable')}</span>
</div>
<div
className="flex items-center cursor-pointer text-red-400 hover:text-red-500"
onClick={handleDeleteClick}

View File

@ -1,46 +1,168 @@
import { Checkbox } from '@/components/ui/checkbox';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { CheckedState } from '@radix-ui/react-checkbox';
import { useState } from 'react';
interface FormatPreserveEditorProps {
initialValue: string;
onSave: (value: string) => void;
initialValue: {
key: string;
type: string;
value: Array<{ [key: string]: string }>;
};
onSave: (value: any) => void;
className?: string;
isSelect?: boolean;
isDelete?: boolean;
isChunck?: boolean;
handleCheckboxClick?: (id: string | number, checked: boolean) => void;
selectedChunkIds?: string[];
}
const FormatPreserveEditor = ({
initialValue,
onSave,
className,
isChunck,
handleCheckboxClick,
selectedChunkIds,
}: FormatPreserveEditorProps) => {
const [content, setContent] = useState(initialValue);
const [isEditing, setIsEditing] = useState(false);
const handleEdit = () => setIsEditing(true);
const handleSave = () => {
onSave(content);
setIsEditing(false);
// const [isEditing, setIsEditing] = useState(false);
const [activeEditIndex, setActiveEditIndex] = useState<number | undefined>(
undefined,
);
console.log('initialValue', initialValue);
const handleEdit = (e?: any, index?: number) => {
console.log(e, index, content);
if (content.key === 'json') {
console.log(e, e.target.innerText);
setContent((pre) => ({
...pre,
value: pre.value.map((item, i) => {
if (i === index) {
return {
...item,
[Object.keys(item)[0]]: e.target.innerText,
};
}
return item;
}),
}));
setActiveEditIndex(index);
}
};
const handleChange = (e: any) => {
if (content.key === 'json') {
setContent((pre) => ({
...pre,
value: pre.value.map((item, i) => {
if (i === activeEditIndex) {
return {
...item,
[Object.keys(item)[0]]: e.target.value,
};
}
return item;
}),
}));
} else {
setContent(e.target.value);
}
};
const escapeNewlines = (text: string) => {
return text.replace(/\n/g, '\\n');
};
const unescapeNewlines = (text: string) => {
return text.replace(/\\n/g, '\n');
};
const handleSave = () => {
const saveData = {
...content,
value: content.value?.map((item) => {
return { ...item, text: unescapeNewlines(item.text) };
}),
};
onSave(saveData);
setActiveEditIndex(undefined);
};
const handleCheck = (e: CheckedState, id: string | number) => {
handleCheckboxClick?.(id, e === 'indeterminate' ? false : e);
};
return (
<div className="editor-container">
{isEditing ? (
{/* {isEditing && content.key === 'json' ? (
<Textarea
className={cn(
'w-full h-full bg-transparent text-text-secondary',
'w-full h-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none min-h-6 p-0',
className,
)}
value={content}
onChange={(e) => setContent(e.target.value)}
value={content.value}
onChange={handleChange}
onBlur={handleSave}
autoSize={{ maxRows: 100 }}
autoFocus
/>
) : (
<pre className="text-text-secondary" onClick={handleEdit}>
{content}
</pre>
)}
<>
{content.key === 'json' && */}
{content.value.map((item, index) => (
<section
key={index}
className={
isChunck
? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start'
: ''
}
>
{isChunck && (
<Checkbox
onCheckedChange={(e) => {
handleCheck(e, index);
}}
checked={selectedChunkIds?.some(
(id) => id.toString() === index.toString(),
)}
></Checkbox>
)}
{activeEditIndex === index && (
<Textarea
key={'t' + index}
className={cn(
'w-full bg-transparent text-text-secondary border-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none !h-6 min-h-6 p-0',
className,
)}
value={escapeNewlines(content.value[index].text)}
onChange={handleChange}
onBlur={handleSave}
autoSize={{ maxRows: 100, minRows: 1 }}
autoFocus
/>
)}
{activeEditIndex !== index && (
<div
className="text-text-secondary overflow-auto scrollbar-auto whitespace-pre-wrap"
key={index}
onClick={(e) => {
handleEdit(e, index);
}}
>
{escapeNewlines(item.text)}
</div>
)}
</section>
))}
{/* {content.key !== 'json' && (
<pre
className="text-text-secondary overflow-auto scrollbar-auto"
onClick={handleEdit}
>
</pre>
)}
</>
)}*/}
</div>
);
};

View File

@ -4,16 +4,15 @@ import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import { CircleAlert } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useRerunDataflow } from '../../hooks';
interface RerunButtonProps {
className?: string;
step?: TimelineNode;
onRerun?: () => void;
loading?: boolean;
}
const RerunButton = (props: RerunButtonProps) => {
const { className, step, onRerun } = props;
const { className, step, onRerun, loading } = props;
const { t } = useTranslation();
const { loading } = useRerunDataflow();
const clickFunc = () => {
console.log('click rerun button');
Modal.show({
@ -29,15 +28,15 @@ const RerunButton = (props: RerunButtonProps) => {
}}
></div>
),
onVisibleChange: () => {
Modal.hide();
},
onOk: () => {
onRerun?.();
Modal.hide();
},
onCancel: () => {
Modal.hide();
okText: t('modal.okText'),
cancelText: t('modal.cancelText'),
onVisibleChange: (visible: boolean) => {
if (!visible) {
Modal.destroy();
} else {
onRerun?.();
Modal.destroy();
}
},
});
};

View File

@ -1,85 +1,112 @@
import { CustomTimeline, TimelineNode } from '@/components/originui/timeline';
import {
CheckLine,
FilePlayIcon,
Grid3x2,
Blocks,
File,
FilePlay,
FileStack,
Heading,
ListPlus,
PlayIcon,
} from 'lucide-react';
import { useMemo } from 'react';
export enum TimelineNodeType {
begin = 'begin',
parser = 'parser',
chunk = 'chunk',
indexer = 'indexer',
complete = 'complete',
end = 'end',
}
export const TimelineNodeArr = [
{
id: 1,
title: 'Begin',
icon: <PlayIcon size={13} />,
import { TimelineNodeType } from '../../constant';
import { IPipelineFileLogDetail } from '../../interface';
export type ITimelineNodeObj = {
title: string;
icon: JSX.Element;
clickable?: boolean;
type: TimelineNodeType;
};
export const TimelineNodeObj = {
[TimelineNodeType.begin]: {
title: 'File',
icon: <File size={13} />,
clickable: false,
type: TimelineNodeType.begin,
},
{
id: 2,
[TimelineNodeType.parser]: {
title: 'Parser',
icon: <FilePlayIcon size={13} />,
type: TimelineNodeType.parser,
icon: <FilePlay size={13} />,
},
{
id: 3,
title: 'Chunker',
icon: <Grid3x2 size={13} />,
type: TimelineNodeType.chunk,
[TimelineNodeType.contextGenerator]: {
title: 'Context Generator',
icon: <FileStack size={13} />,
},
{
id: 4,
title: 'Indexer',
[TimelineNodeType.titleSplitter]: {
title: 'Title Splitter',
icon: <Heading size={13} />,
},
[TimelineNodeType.characterSplitter]: {
title: 'Title Splitter',
icon: <Heading size={13} />,
},
[TimelineNodeType.splitter]: {
title: 'Character Splitter',
icon: <Blocks size={13} />,
},
[TimelineNodeType.tokenizer]: {
title: 'Tokenizer',
icon: <ListPlus size={13} />,
clickable: false,
type: TimelineNodeType.indexer,
},
{
id: 5,
title: 'Complete',
icon: <CheckLine size={13} />,
clickable: false,
type: TimelineNodeType.complete,
},
];
};
// export const TimelineNodeArr = [
// {
// id: 1,
// title: 'File',
// icon: <PlayIcon size={13} />,
// clickable: false,
// type: TimelineNodeType.begin,
// },
// {
// id: 2,
// title: 'Context Generator',
// icon: <PlayIcon size={13} />,
// type: TimelineNodeType.contextGenerator,
// },
// {
// id: 3,
// title: 'Title Splitter',
// icon: <PlayIcon size={13} />,
// type: TimelineNodeType.titleSplitter,
// },
// {
// id: 4,
// title: 'Character Splitter',
// icon: <PlayIcon size={13} />,
// type: TimelineNodeType.characterSplitter,
// },
// {
// id: 5,
// title: 'Tokenizer',
// icon: <CheckLine size={13} />,
// clickable: false,
// type: TimelineNodeType.tokenizer,
// },
// ]
export interface TimelineDataFlowProps {
activeId: number | string;
activeFunc: (id: number | string, step: TimelineNode) => void;
data: IPipelineFileLogDetail;
timelineNodes: TimelineNode[];
}
const TimelineDataFlow = ({ activeFunc, activeId }: TimelineDataFlowProps) => {
// const [activeStep, setActiveStep] = useState(2);
const timelineNodes: TimelineNode[] = useMemo(() => {
const nodes: TimelineNode[] = [];
TimelineNodeArr.forEach((node) => {
nodes.push({
...node,
className: 'w-32',
completed: false,
});
});
return nodes;
}, []);
const TimelineDataFlow = ({
activeFunc,
activeId,
data,
timelineNodes,
}: TimelineDataFlowProps) => {
// const [timelineNodeArr,setTimelineNodeArr] = useState<ITimelineNodeObj & {id: number | string}>()
const activeStep = useMemo(() => {
const index = timelineNodes.findIndex((node) => node.id === activeId);
return index > -1 ? index + 1 : 0;
}, [activeId, timelineNodes]);
const handleStepChange = (step: number, id: string | number) => {
// setActiveStep(step);
activeFunc?.(
id,
timelineNodes.find((node) => node.id === activeStep) as TimelineNode,
);
console.log(step, id);
};
return (

View File

@ -2,3 +2,14 @@ export enum ChunkTextMode {
Full = 'full',
Ellipse = 'ellipse',
}
export enum TimelineNodeType {
begin = 'file',
parser = 'parser',
splitter = 'splitter',
contextGenerator = 'contextGenerator',
titleSplitter = 'titleSplitter',
characterSplitter = 'characterSplitter',
tokenizer = 'tokenizer',
end = 'end',
}

View File

@ -1,4 +1,4 @@
import message from '@/components/ui/message';
import { TimelineNode } from '@/components/originui/timeline';
import {
useCreateChunk,
useDeleteChunk,
@ -8,40 +8,17 @@ import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { IChunk } from '@/interfaces/database/knowledge';
import kbService from '@/services/knowledge-service';
import { formatSecondsToHumanReadable } from '@/utils/date';
import { buildChunkHighlights } from '@/utils/document-util';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { camelCase, upperFirst } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IHighlight } from 'react-pdf-highlighter';
import { useParams, useSearchParams } from 'umi';
import { ChunkTextMode } from './constant';
import { ITimelineNodeObj, TimelineNodeObj } from './components/time-line';
import { ChunkTextMode, TimelineNodeType } from './constant';
import { IDslComponent, IPipelineFileLogDetail } from './interface';
export interface IPipelineFileLogDetail {
avatar: string;
create_date: string;
create_time: number;
document_id: string;
document_name: string;
document_suffix: string;
document_type: string;
dsl: string;
id: string;
kb_id: string;
operation_status: string;
parser_id: string;
pipeline_id: string;
pipeline_title: string;
process_begin_at: string;
process_duration: number;
progress: number;
progress_msg: string;
source_from: string;
status: string;
task_type: string;
tenant_id: string;
update_date: string;
update_time: number;
}
export const useFetchPipelineFileLogDetail = (props?: {
isEdit?: boolean;
refreshCount?: number;
@ -199,52 +176,105 @@ export const useFetchParserList = () => {
};
};
export const useRerunDataflow = () => {
export const useRerunDataflow = ({
data,
}: {
data: IPipelineFileLogDetail;
}) => {
const [loading, setLoading] = useState(false);
const [isChange, setIsChange] = useState(false);
const handleReRunFunc = useCallback(
(newData: { value: IDslComponent; key: string }) => {
const newDsl = {
...data.dsl,
components: {
...data.dsl.components,
[newData.key]: newData.value,
},
};
// this Data provided to the interface
const params = {
id: data.id,
dsl: newDsl,
compenent_id: newData.key,
};
console.log('newDsl', newDsl, params);
},
[data],
);
return {
loading,
setLoading,
isChange,
setIsChange,
handleReRunFunc,
};
};
export const useFetchPaserText = () => {
const initialText =
'第一行文本\n\t第二行缩进文本\n第三行 多个空格 第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格';
const [loading, setLoading] = useState(false);
const [data, setData] = useState<string>(initialText);
const { t } = useTranslation();
const queryClient = useQueryClient();
export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => {
const timelineNodes: TimelineNode[] = useMemo(() => {
const nodes: Array<ITimelineNodeObj & { id: number | string }> = [];
console.log('time-->', data);
const times = data?.dsl?.components;
if (times) {
const getNode = (
key: string,
index: number,
type:
| TimelineNodeType.begin
| TimelineNodeType.parser
| TimelineNodeType.splitter
| TimelineNodeType.tokenizer
| TimelineNodeType.characterSplitter
| TimelineNodeType.titleSplitter,
) => {
const node = times[key].obj;
const name = camelCase(
node.component_name,
) as keyof typeof TimelineNodeObj;
const {
// data,
// isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['createChunk'],
mutationFn: async (payload: any) => {
// let service = kbService.create_chunk;
// if (payload.chunk_id) {
// service = kbService.set_chunk;
// }
// const { data } = await service(payload);
// if (data.code === 0) {
message.success(t('message.created'));
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] });
}, 1000); // Delay to ensure the list is updated
// }
// return data?.code;
},
});
let tempType = type;
if (name === TimelineNodeType.parser) {
tempType = TimelineNodeType.parser;
} else if (name === TimelineNodeType.tokenizer) {
tempType = TimelineNodeType.tokenizer;
} else if (
name === TimelineNodeType.characterSplitter ||
name === TimelineNodeType.titleSplitter ||
name === TimelineNodeType.splitter
) {
tempType = TimelineNodeType.splitter;
}
const timeNode = {
...TimelineNodeObj[name],
clickable: true,
id: index,
className: 'w-32',
completed: false,
date: formatSecondsToHumanReadable(
node.params?.outputs?._elapsed_time?.value || 0,
),
type: tempType,
detail: { value: times[key], key: key },
};
console.log('timeNodetype-->', type);
nodes.push(timeNode);
return { data, loading, rerun: mutateAsync };
if (times[key].downstream && times[key].downstream.length > 0) {
const nextKey = times[key].downstream[0];
// nodes.push(timeNode);
getNode(nextKey, index + 1, tempType);
}
};
getNode(upperFirst(TimelineNodeType.begin), 1, TimelineNodeType.begin);
// setTimelineNodeArr(nodes as unknown as ITimelineNodeObj & {id: number | string})
}
return nodes;
}, [data]);
return {
timelineNodes,
};
};

View File

@ -7,6 +7,7 @@ import {
useGetChunkHighlights,
useHandleChunkCardClick,
useRerunDataflow,
useTimelineDataFlow,
} from './hooks';
import DocumentHeader from './components/document-preview/document-header';
@ -29,13 +30,11 @@ import {
useNavigatePage,
} from '@/hooks/logic-hooks/navigate-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { ChunkerContainer } from './chunker';
import { useGetDocumentUrl } from './components/document-preview/hooks';
import TimelineDataFlow, {
TimelineNodeArr,
TimelineNodeType,
} from './components/time-line';
import TimelineDataFlow from './components/time-line';
import { TimelineNodeType } from './constant';
import styles from './index.less';
import { IDslComponent } from './interface';
import ParserContainer from './parser';
const Chunk = () => {
@ -45,9 +44,10 @@ const Chunk = () => {
const { selectedChunkId } = useHandleChunkCardClick();
const [activeStepId, setActiveStepId] = useState<number | string>(0);
const { data: dataset } = useFetchPipelineFileLogDetail();
const { isChange, setIsChange } = useRerunDataflow();
const { t } = useTranslation();
const { timelineNodes } = useTimelineDataFlow(dataset);
const { navigateToDataset, getQueryString, navigateToDatasetList } =
useNavigatePage();
const fileUrl = useGetDocumentUrl();
@ -69,8 +69,15 @@ const Chunk = () => {
return 'unknown';
}, [documentInfo]);
const {
handleReRunFunc,
isChange,
setIsChange,
loading: reRunLoading,
} = useRerunDataflow({
data: dataset,
});
const handleStepChange = (id: number | string, step: TimelineNode) => {
console.log(id, step);
if (isChange) {
Modal.show({
visible: true,
@ -114,12 +121,14 @@ const Chunk = () => {
};
const { type } = useGetKnowledgeSearchParams();
const currentTimeNode: TimelineNode = useMemo(() => {
return (
TimelineNodeArr.find((node) => node.id === activeStepId) ||
timelineNodes.find((node) => node.id === activeStepId) ||
({} as TimelineNode)
);
}, [activeStepId]);
}, [activeStepId, timelineNodes]);
return (
<>
<PageHeader>
@ -134,10 +143,10 @@ const Chunk = () => {
<BreadcrumbItem>
<BreadcrumbLink
onClick={navigateToDataset(
getQueryString(QueryStringMap.id) as string,
getQueryString(QueryStringMap.KnowledgeId) as string,
)}
>
{dataset.name}
{t('knowledgeDetails.overview')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
@ -152,6 +161,8 @@ const Chunk = () => {
<TimelineDataFlow
activeFunc={handleStepChange}
activeId={activeStepId}
data={dataset}
timelineNodes={timelineNodes}
/>
</div>
)}
@ -173,20 +184,31 @@ const Chunk = () => {
</div>
<div className="h-dvh border-r -mt-3"></div>
<div className="w-3/5 h-full">
{currentTimeNode?.type === TimelineNodeType.chunk && (
{/* {currentTimeNode?.type === TimelineNodeType.splitter && (
<ChunkerContainer
isChange={isChange}
setIsChange={setIsChange}
step={currentTimeNode as TimelineNode}
/>
)}
{currentTimeNode?.type === TimelineNodeType.parser && (
)} */}
{/* {currentTimeNode?.type === TimelineNodeType.parser && ( */}
{(currentTimeNode?.type === TimelineNodeType.parser ||
currentTimeNode?.type === TimelineNodeType.splitter) && (
<ParserContainer
isChange={isChange}
reRunLoading={reRunLoading}
setIsChange={setIsChange}
step={currentTimeNode as TimelineNode}
data={
currentTimeNode.detail as {
value: IDslComponent;
key: string;
}
}
reRunFunc={handleReRunFunc}
/>
)}
{/* )} */}
<Spotlight opcity={0.6} coverage={60} />
</div>
</div>

View File

@ -0,0 +1,62 @@
interface ComponentParams {
debug_inputs: Record<string, any>;
delay_after_error: number;
description: string;
exception_default_value: any;
exception_goto: any;
exception_method: any;
inputs: Record<string, any>;
max_retries: number;
message_history_window_size: number;
outputs: {
_created_time: Record<string, any>;
_elapsed_time: Record<string, any>;
name: Record<string, any>;
output_format: { type: string; value: string };
json: { type: string; value: string };
};
persist_logs: boolean;
timeout: number;
}
interface ComponentObject {
component_name: string;
params: ComponentParams;
}
export interface IDslComponent {
downstream: Array<string>;
obj: ComponentObject;
upstream: Array<string>;
}
export interface IPipelineFileLogDetail {
avatar: string;
create_date: string;
create_time: number;
document_id: string;
document_name: string;
document_suffix: string;
document_type: string;
dsl: {
components: {
[key: string]: IDslComponent;
};
task_id: string;
path: Array<string>;
};
id: string;
kb_id: string;
operation_status: string;
parser_id: string;
pipeline_id: string;
pipeline_title: string;
process_begin_at: string;
process_duration: number;
progress: number;
progress_msg: string;
source_from: string;
status: string;
task_type: string;
tenant_id: string;
update_date: string;
update_time: number;
}

View File

@ -2,41 +2,121 @@ import { TimelineNode } from '@/components/originui/timeline';
import Spotlight from '@/components/spotlight';
import { Spin } from '@/components/ui/spin';
import classNames from 'classnames';
import { useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import FormatPreserveEditor from './components/parse-editer';
import CheckboxSets from './components/chunk-result-bar/checkbox-sets';
import FormatPreserEditor from './components/parse-editer';
import RerunButton from './components/rerun-button';
import { useFetchParserList, useFetchPaserText } from './hooks';
import { TimelineNodeType } from './constant';
import { useFetchParserList } from './hooks';
import { IDslComponent } from './interface';
interface IProps {
isChange: boolean;
setIsChange: (isChange: boolean) => void;
step?: TimelineNode;
data: { value: IDslComponent; key: string };
reRunLoading: boolean;
reRunFunc: (data: { value: IDslComponent; key: string }) => void;
}
const ParserContainer = (props: IProps) => {
const { isChange, setIsChange, step } = props;
const { data: initialValue, rerun: onSave } = useFetchPaserText();
const { isChange, setIsChange, step, data, reRunFunc, reRunLoading } = props;
const { t } = useTranslation();
const { loading } = useFetchParserList();
const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]);
const initialValue = useMemo(() => {
const outputs = data?.value?.obj?.params?.outputs;
const key = outputs?.output_format?.value;
const value = outputs[key]?.value;
const type = outputs[key]?.type;
console.log('outputs-->', outputs);
return {
key,
type,
value,
};
}, [data]);
const [initialText, setInitialText] = useState(initialValue);
const handleSave = (newContent: string) => {
console.log('保存内容:', newContent);
if (newContent !== initialText) {
const handleSave = (newContent: any) => {
console.log('newContent-change-->', newContent, initialValue);
if (JSON.stringify(newContent) !== JSON.stringify(initialValue)) {
setIsChange(true);
onSave(newContent);
setInitialText(newContent);
} else {
setIsChange(false);
}
// Here, the API is called to send newContent to the backend
};
const handleReRunFunc = () => {
const handleReRunFunc = useCallback(() => {
const newData: { value: IDslComponent; key: string } = {
...data,
value: {
...data.value,
obj: {
...data.value.obj,
params: {
...(data.value?.obj?.params || {}),
outputs: {
...(data.value?.obj?.params?.outputs || {}),
[initialText.key]: {
type: initialText.type,
value: initialText.value,
},
},
},
},
},
};
reRunFunc(newData);
setIsChange(false);
};
}, [data, initialText, reRunFunc, setIsChange]);
const handleRemoveChunk = useCallback(async () => {
if (selectedChunkIds.length > 0) {
initialText.value = initialText.value.filter(
(item: any, index: number) => !selectedChunkIds.includes(index + ''),
);
setSelectedChunkIds([]);
}
}, [selectedChunkIds, initialText]);
const handleCheckboxClick = useCallback(
(id: string | number, checked: boolean) => {
console.log('handleCheckboxClick', id, checked, selectedChunkIds);
setSelectedChunkIds((prev) => {
if (checked) {
return [...prev, id.toString()];
} else {
return prev.filter((item) => item.toString() !== id.toString());
}
});
},
[],
);
const selectAllChunk = useCallback(
(checked: boolean) => {
setSelectedChunkIds(
checked ? initialText.value.map((x, index: number) => index) : [],
);
},
[initialText.value],
);
const isChunck =
step?.type === TimelineNodeType.characterSplitter ||
step?.type === TimelineNodeType.titleSplitter ||
step?.type === TimelineNodeType.splitter;
return (
<>
{isChange && (
<div className=" absolute top-2 right-6">
<RerunButton step={step} onRerun={handleReRunFunc} />
<RerunButton
step={step}
onRerun={handleReRunFunc}
loading={reRunLoading}
/>
</div>
)}
<div className={classNames('flex flex-col w-full')}>
@ -51,11 +131,33 @@ const ParserContainer = (props: IProps) => {
</div>
</div>
</div>
<div className=" border rounded-lg p-[20px] box-border h-[calc(100vh-180px)] overflow-auto scrollbar-none">
<FormatPreserveEditor
{isChunck && (
<div className="pt-[5px] pb-[5px]">
<CheckboxSets
selectAllChunk={selectAllChunk}
removeChunk={handleRemoveChunk}
checked={selectedChunkIds.length === initialText.value.length}
selectedChunkIds={selectedChunkIds}
/>
</div>
)}
<div className=" border rounded-lg p-[20px] box-border h-[calc(100vh-180px)] w-[calc(100%-20px)] overflow-auto scrollbar-none">
<FormatPreserEditor
initialValue={initialText}
onSave={handleSave}
className="!h-[calc(100vh-220px)]"
className={
initialText.key !== 'json' ? '!h-[calc(100vh-220px)]' : ''
}
isChunck={isChunck}
isDelete={
step?.type === TimelineNodeType.characterSplitter ||
step?.type === TimelineNodeType.titleSplitter ||
step?.type === TimelineNodeType.splitter
}
handleCheckboxClick={handleCheckboxClick}
selectedChunkIds={selectedChunkIds}
/>
<Spotlight opcity={0.6} coverage={60} />
</div>

View File

@ -4,10 +4,12 @@ import {
} from '@/hooks/logic-hooks';
import kbService, {
listDataPipelineLogDocument,
listPipelineDatasetLogs,
} from '@/services/knowledge-service';
import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import { useParams, useSearchParams } from 'umi';
import { LogTabs } from './dataset-common';
export interface IOverviewTital {
cancelled: number;
@ -61,7 +63,7 @@ export interface IFileLogItem {
update_time: number;
}
export interface IFileLogList {
docs: IFileLogItem[];
logs: IFileLogItem[];
total: number;
}
@ -70,7 +72,14 @@ const useFetchFileLogList = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const { id } = useParams();
const [active, setActive] = useState<(typeof LogTabs)[keyof typeof LogTabs]>(
LogTabs.FILE_LOGS,
);
const knowledgeBaseId = searchParams.get('id') || id;
const fetchFunc =
active === LogTabs.DATASET_LOGS
? listPipelineDatasetLogs
: listDataPipelineLogDocument;
const { data } = useQuery<IFileLogList>({
queryKey: [
'fileLogList',
@ -78,9 +87,10 @@ const useFetchFileLogList = () => {
pagination.current,
pagination.pageSize,
searchString,
active,
],
queryFn: async () => {
const { data: res = {} } = await listDataPipelineLogDocument({
const { data: res = {} } = await fetchFunc({
kb_id: knowledgeBaseId,
page: pagination.current,
page_size: pagination.pageSize,
@ -102,6 +112,8 @@ const useFetchFileLogList = () => {
searchString,
handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.total },
active,
setActive,
};
};

View File

@ -71,9 +71,7 @@ const CardFooterProcess: FC<CardFooterProcessProps> = ({
};
const FileLogsPage: FC = () => {
const { t } = useTranslation();
const [active, setActive] = useState<(typeof LogTabs)[keyof typeof LogTabs]>(
LogTabs.FILE_LOGS,
);
const [topAllData, setTopAllData] = useState({
totalFiles: {
value: 0,
@ -111,12 +109,14 @@ const FileLogsPage: FC = () => {
searchString,
handleInputChange,
pagination,
active,
setActive,
} = useFetchFileLogList();
const tableList = useMemo(() => {
console.log('tableList', tableOriginData);
if (tableOriginData && tableOriginData.docs?.length) {
return tableOriginData.docs.map((item) => {
if (tableOriginData && tableOriginData.logs?.length) {
return tableOriginData.logs.map((item) => {
return {
...item,
fileName: item.document_name,

View File

@ -31,6 +31,7 @@ import {
import { TFunction } from 'i18next';
import { ClipboardList, Eye } from 'lucide-react';
import { FC, useMemo, useState } from 'react';
import { useParams } from 'umi';
import { RunningStatus } from '../dataset/constant';
import ProcessLogModal from '../process-log-modal';
import { LogTabs, ProcessingType } from './dataset-common';
@ -58,9 +59,11 @@ interface FileLogsTableProps {
export const getFileLogsTableColumns = (
t: TFunction<'translation', string>,
showLog: (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => void,
kowledgeId: string,
navigateToDataflowResult: (
id: string,
knowledgeId?: string | undefined,
knowledgeId: string,
doc_id?: string,
) => () => void,
) => {
// const { t } = useTranslate('knowledgeDetails');
@ -175,7 +178,11 @@ export const getFileLogsTableColumns = (
variant="ghost"
size="sm"
className="p-1"
onClick={navigateToDataflowResult(row.original.id)}
onClick={navigateToDataflowResult(
row.original.id,
kowledgeId,
row.original.document_id,
)}
>
<ClipboardList />
</Button>
@ -288,7 +295,8 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
const [isModalVisible, setIsModalVisible] = useState(false);
const { navigateToDataflowResult } = useNavigatePage();
const [logInfo, setLogInfo] = useState<IFileLogItem>({});
const showLog = (row: Row<IFileLogItem & DocumentLog>, active: LogTabs) => {
const kowledgeId = useParams().id;
const showLog = (row: Row<IFileLogItem & DocumentLog>) => {
const logDetail = {
taskId: row.original.id,
fileName: row.original.document_name,
@ -306,7 +314,12 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
const columns = useMemo(() => {
return active === LogTabs.FILE_LOGS
? getFileLogsTableColumns(t, showLog, navigateToDataflowResult)
? getFileLogsTableColumns(
t,
showLog,
kowledgeId || '',
navigateToDataflowResult,
)
: getDatasetLogsTableColumns(t, showLog);
}, [active, t]);

View File

@ -80,7 +80,7 @@ export const useShowLog = (documents: IDocumentInfo[]) => {
fileSize: findRecord.size + '',
source: findRecord.source_type,
task: findRecord.status,
state: findRecord.run,
status: findRecord.run,
startTime: findRecord.process_begin_at,
endTime: findRecord.process_begin_at,
duration: findRecord.process_duration + 's',

View File

@ -7,11 +7,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { Progress } from '@/components/ui/progress';
import { Separator } from '@/components/ui/separator';
import { IDocumentInfo } from '@/interfaces/database/document';
@ -19,7 +14,7 @@ import { CircleX } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { DocumentType, RunningStatus } from './constant';
import { ParsingCard, PopoverContent } from './parsing-card';
import { ParsingCard } from './parsing-card';
import { UseChangeDocumentParserShowType } from './use-change-document-parser';
import { useHandleRunDocumentByIds } from './use-run-document';
import { UseSaveMetaShowType } from './use-save-meta';
@ -122,23 +117,13 @@ export function ParsingStatusCell({
)}
{isParserRunning(run) ? (
<>
<HoverCard>
<HoverCardTrigger asChild>
<div
className="flex items-center gap-1"
onClick={() => handleShowLog(record)}
>
<Progress value={p} className="h-1 flex-1 min-w-10" />
{p}%
</div>
</HoverCardTrigger>
<HoverCardContent className="w-[40vw]">
<PopoverContent
record={record}
handleShowLog={handleShowLog}
></PopoverContent>
</HoverCardContent>
</HoverCard>
<div
className="flex items-center gap-1 cursor-pointer"
onClick={() => handleShowLog(record)}
>
<Progress value={p} className="h-1 flex-1 min-w-10" />
{p}%
</div>
<div
className="cursor-pointer flex items-center gap-3"
onClick={

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

@ -41,6 +41,7 @@ const {
retrievalTestShare,
getKnowledgeBasicInfo,
fetchDataPipelineLog,
fetchPipelineDatasetLogs,
} = api;
const methods = {
@ -179,6 +180,10 @@ const methods = {
url: fetchDataPipelineLog,
method: 'post',
},
fetchPipelineDatasetLogs: {
url: fetchPipelineDatasetLogs,
method: 'post',
},
get_pipeline_detail: {
url: api.get_pipeline_detail,
method: 'get',
@ -223,5 +228,9 @@ export const listDataPipelineLogDocument = (
params?: IFetchKnowledgeListRequestParams,
body?: IFetchDocumentListRequestBody,
) => request.post(api.fetchDataPipelineLog, { data: body || {}, params });
export const listPipelineDatasetLogs = (
params?: IFetchKnowledgeListRequestParams,
body?: IFetchDocumentListRequestBody,
) => request.post(api.fetchPipelineDatasetLogs, { data: body || {}, params });
export default kbService;

View File

@ -49,6 +49,7 @@ export default {
// data pipeline log
fetchDataPipelineLog: `${api_host}/kb/list_pipeline_logs`,
get_pipeline_detail: `${api_host}/kb/pipeline_log_detail`,
fetchPipelineDatasetLogs: `${api_host}/kb/list_pipeline_dataset_logs`,
// tags
listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`,
@ -169,6 +170,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

@ -1,4 +1,5 @@
import dayjs from 'dayjs';
import { toFixed } from './common-util';
export function formatDate(date: any) {
if (!date) {
@ -43,3 +44,20 @@ export function formatStandardDate(date: any) {
}
return parsedDate.format('YYYY-MM-DD');
}
export function formatSecondsToHumanReadable(seconds: number): string {
if (isNaN(seconds) || seconds < 0) {
return '0s';
}
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = toFixed(seconds % 60, 3);
const parts = [];
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
if (s || parts.length === 0) parts.push(`${s}s`);
return parts.join('');
}

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;