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

View File

@ -28,7 +28,7 @@ const DualRangeSlider = React.forwardRef<
)} )}
{...props} {...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.Range className="absolute h-full bg-accent-primary" />
</SliderPrimitive.Track> </SliderPrimitive.Track>
{initialValue.map((value, index) => ( {initialValue.map((value, index) => (

View File

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

View File

@ -1,3 +1,6 @@
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
import { ChatVariableEnabledField, variableEnabledFieldMap } from './chat';
export enum ProgrammingLanguage { export enum ProgrammingLanguage {
Python = 'python', Python = 'python',
Javascript = 'javascript', Javascript = 'javascript',
@ -26,3 +29,21 @@ export enum AgentGlobals {
} }
export const AgentGlobalsSysQueryWithBrace = `{${AgentGlobals.SysQuery}}`; 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( const navigateToDataflowResult = useCallback(
(id: string, knowledgeId?: string) => () => { (id: string, knowledgeId: string, doc_id?: string) => () => {
navigate( navigate(
// `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`, // `${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], [navigate],

View File

@ -52,6 +52,7 @@ export const enum AgentApiAction {
FetchExternalAgentInputs = 'fetchExternalAgentInputs', FetchExternalAgentInputs = 'fetchExternalAgentInputs',
SetAgentSetting = 'setAgentSetting', SetAgentSetting = 'setAgentSetting',
FetchPrompt = 'fetchPrompt', FetchPrompt = 'fetchPrompt',
CancelDataflow = 'cancelDataflow',
} }
export const EmptyDsl = { export const EmptyDsl = {
@ -387,7 +388,7 @@ export const useUploadCanvasFileWithProgress = (
files.forEach((file) => { files.forEach((file) => {
onError(file, error as Error); 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 = () => { export const useTestDbConnect = () => {
@ -571,7 +572,6 @@ export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => {
initialData: {} as IAgentLogsResponse, initialData: {} as IAgentLogsResponse,
gcTime: 0, gcTime: 0,
queryFn: async () => { queryFn: async () => {
console.log('useFetchAgentLog', searchParams);
const { data } = await fetchAgentLogsByCanvasId(id as string, { const { data } = await fetchAgentLogsByCanvasId(id as string, {
...searchParams, ...searchParams,
}); });
@ -678,3 +678,24 @@ export const useFetchAgentList = ({
return { data, loading }; 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: sqlStatementTip:
'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.', 'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.',
frameworkPrompts: 'Framework', frameworkPrompts: 'Framework',
release: 'Publish',
}, },
llmTools: { llmTools: {
bad_calculator: { bad_calculator: {
@ -1702,6 +1703,15 @@ This delimiter is used to split the input text into several text pieces echo of
begin: 'File', begin: 'File',
parserMethod: 'Parser method', parserMethod: 'Parser method',
exportJson: 'Export JSON', 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: sqlStatementTip:
'在此处编写您的 SQL 查询。您可以使用变量、原始 SQL或使用变量语法混合使用两者。', '在此处编写您的 SQL 查询。您可以使用变量、原始 SQL或使用变量语法混合使用两者。',
frameworkPrompts: '框架', frameworkPrompts: '框架',
release: '发布',
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',
@ -1620,6 +1621,15 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
begin: '文件', begin: '文件',
parserMethod: '解析方法', parserMethod: '解析方法',
exportJson: '导出 JSON', exportJson: '导出 JSON',
viewResult: '查看结果',
running: '运行中',
context: '上下文生成器',
contextDescription: '上下文生成器',
summary: '摘要',
keywords: '关键词',
questions: '问题',
metadata: '元数据',
fieldName: '结果目的地',
}, },
}, },
}; };

View File

@ -7,6 +7,7 @@ import {
AgentGlobalsSysQueryWithBrace, AgentGlobalsSysQueryWithBrace,
CodeTemplateStrMap, CodeTemplateStrMap,
ProgrammingLanguage, ProgrammingLanguage,
initialLlmBaseValues,
} from '@/constants/agent'; } from '@/constants/agent';
export enum AgentDialogueMode { export enum AgentDialogueMode {
@ -14,13 +15,8 @@ export enum AgentDialogueMode {
Task = 'task', Task = 'task',
} }
import {
ChatVariableEnabledField,
variableEnabledFieldMap,
} from '@/constants/chat';
import { ModelVariableType } from '@/constants/knowledge'; import { ModelVariableType } from '@/constants/knowledge';
import i18n from '@/locales/config'; import i18n from '@/locales/config';
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
import { t } from 'i18next'; import { t } from 'i18next';
// DuckDuckGo's channel options // DuckDuckGo's channel options
@ -271,24 +267,6 @@ export const initialBeginValues = {
prologue: `Hi! I'm your assistant. What can I do for you?`, 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 = { export const initialGenerateValues = {
...initialLlmBaseValues, ...initialLlmBaseValues,
prompt: i18n.t('flow.promptText'), prompt: i18n.t('flow.promptText'),

View File

@ -35,12 +35,12 @@ import {
useHideFormSheetOnNodeDeletion, useHideFormSheetOnNodeDeletion,
useShowDrawer, useShowDrawer,
} from '../hooks/use-show-drawer'; } from '../hooks/use-show-drawer';
import { LogSheet } from '../log-sheet';
import RunSheet from '../run-sheet'; import RunSheet from '../run-sheet';
import { ButtonEdge } from './edge'; import { ButtonEdge } from './edge';
import styles from './index.less'; import styles from './index.less';
import { RagNode } from './node'; import { RagNode } from './node';
import { BeginNode } from './node/begin-node'; import { BeginNode } from './node/begin-node';
import { ContextNode } from './node/context-node';
import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown'; import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown';
import { HierarchicalMergerNode } from './node/hierarchical-merger-node'; import { HierarchicalMergerNode } from './node/hierarchical-merger-node';
import NoteNode from './node/note-node'; import NoteNode from './node/note-node';
@ -56,6 +56,7 @@ export const nodeTypes: NodeTypes = {
tokenizerNode: TokenizerNode, tokenizerNode: TokenizerNode,
splitterNode: SplitterNode, splitterNode: SplitterNode,
hierarchicalMergerNode: HierarchicalMergerNode, hierarchicalMergerNode: HierarchicalMergerNode,
contextNode: ContextNode,
}; };
const edgeTypes = { const edgeTypes = {
@ -65,9 +66,10 @@ const edgeTypes = {
interface IProps { interface IProps {
drawerVisible: boolean; drawerVisible: boolean;
hideDrawer(): void; hideDrawer(): void;
showLogSheet(): void;
} }
function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) { function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
nodes, nodes,
@ -147,17 +149,10 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
clearActiveDropdown, clearActiveDropdown,
]); ]);
const { const { run, loading: running } = useRunDataflow(
visible: logSheetVisible, showLogSheet!,
showModal: showLogSheet, hideRunOrChatDrawer,
hideModal: hideLogSheet, );
} = useSetModalState();
const {
run,
loading: running,
messageId,
} = useRunDataflow(showLogSheet!, hideRunOrChatDrawer);
const onConnect = (connection: Connection) => { const onConnect = (connection: Connection) => {
originalOnConnect(connection); originalOnConnect(connection);
@ -311,9 +306,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
loading={running} loading={running}
></RunSheet> ></RunSheet>
)} )}
{logSheetVisible && ( {/* {logSheetVisible && <LogSheet hideModal={hideLogSheet}></LogSheet>} */}
<LogSheet hideModal={hideLogSheet} messageId={messageId}></LogSheet>
)}
</div> </div>
); );
} }

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ export function ToolBar({
children, children,
label, label,
id, id,
showRun = true, showRun = false,
}: ToolBarProps) { }: ToolBarProps) {
const deleteNodeById = useGraphStore((store) => store.deleteNodeById); 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 { import {
ChatVariableEnabledField, ChatVariableEnabledField,
variableEnabledFieldMap, variableEnabledFieldMap,
@ -15,6 +17,89 @@ import {
WrapText, WrapText,
} from 'lucide-react'; } 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 { export enum PromptRole {
User = 'user', User = 'user',
Assistant = 'assistant', Assistant = 'assistant',
@ -34,6 +119,7 @@ export enum Operator {
Tokenizer = 'Tokenizer', Tokenizer = 'Tokenizer',
Splitter = 'Splitter', Splitter = 'Splitter',
HierarchicalMerger = 'HierarchicalMerger', HierarchicalMerger = 'HierarchicalMerger',
Context = 'Context',
} }
export const SwitchLogicOperatorOptions = ['and', 'or']; export const SwitchLogicOperatorOptions = ['and', 'or'];
@ -76,9 +162,34 @@ export enum ImageParseMethod {
OCR = 'ocr', 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 = { export const initialBeginValues = {
mode: AgentDialogueMode.Conversational, outputs: {
prologue: `Hi! I'm your assistant. What can I do for you?`, name: {
type: 'string',
value: '',
},
file: {
type: 'Object',
value: {},
},
},
}; };
export const variableCheckBoxFieldMap = Object.keys( export const variableCheckBoxFieldMap = Object.keys(
@ -100,7 +211,7 @@ export const initialTokenizerValues = {
TokenizerSearchMethod.FullText, TokenizerSearchMethod.FullText,
], ],
filename_embd_weight: 0.1, filename_embd_weight: 0.1,
fields: ['text'], fields: TokenizerFields.Text,
outputs: {}, outputs: {},
}; };
@ -118,10 +229,40 @@ export enum StringTransformDelimiter {
Space = ' ', 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 = { export const initialSplitterValues = {
outputs: {}, outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
chunk_token_size: 512, chunk_token_size: 512,
overlapped_percent: 0, overlapped_percent: 0,
delimiters: [{ value: '\n' }], delimiters: [{ value: '\n' }],
@ -136,7 +277,9 @@ export enum Hierarchy {
} }
export const initialHierarchicalMergerValues = { export const initialHierarchicalMergerValues = {
outputs: {}, outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
hierarchy: Hierarchy.H3, hierarchy: Hierarchy.H3,
levels: [ levels: [
{ expressions: [{ expression: '^#[^#]' }] }, { expressions: [{ expression: '^#[^#]' }] },
@ -146,6 +289,12 @@ export const initialHierarchicalMergerValues = {
], ],
}; };
export const initialContextValues = {
...initialLlmBaseValues,
field_name: [ContextGeneratorFieldName.Summary],
outputs: {},
};
export const CategorizeAnchorPointPositions = [ export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 }, { top: 1, right: 34 },
{ top: 8, right: 18 }, { top: 8, right: 18 },
@ -178,6 +327,7 @@ export const NodeMap = {
[Operator.Tokenizer]: 'tokenizerNode', [Operator.Tokenizer]: 'tokenizerNode',
[Operator.Splitter]: 'splitterNode', [Operator.Splitter]: 'splitterNode',
[Operator.HierarchicalMerger]: 'hierarchicalMergerNode', [Operator.HierarchicalMerger]: 'hierarchicalMergerNode',
[Operator.Context]: 'contextNode',
}; };
export enum BeginQueryType { export enum BeginQueryType {
@ -220,18 +370,6 @@ export enum AgentExceptionMethod {
Goto = 'goto', 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 = { export const FileTypeSuffixMap = {
[FileType.PDF]: ['pdf'], [FileType.PDF]: ['pdf'],
[FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'], [FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'],

View File

@ -48,3 +48,10 @@ export type HandleContextType = {
export const HandleContext = createContext<HandleContextType>( export const HandleContext = createContext<HandleContextType>(
{} as 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 { Operator } from '../constant';
import ContextForm from '../form/context-form';
import HierarchicalMergerForm from '../form/hierarchical-merger-form'; import HierarchicalMergerForm from '../form/hierarchical-merger-form';
import ParserForm from '../form/parser-form'; import ParserForm from '../form/parser-form';
import SplitterForm from '../form/splitter-form'; import SplitterForm from '../form/splitter-form';
@ -23,4 +24,7 @@ export const FormConfigMap = {
[Operator.HierarchicalMerger]: { [Operator.HierarchicalMerger]: {
component: HierarchicalMergerForm, 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 { RAGFlowFormItem } from '@/components/ragflow-form';
import { buildOptions } from '@/utils/form'; import { buildOptions } from '@/utils/form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FileType } from '../../constant'; import { FileType, OutputFormatMap } from '../../constant';
import { OutputFormatMap } from './constant';
import { CommonProps } from './interface'; import { CommonProps } from './interface';
import { buildFieldNameWithPrefix } from './utils'; 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 { RAGFlowFormItem } from '@/components/ragflow-form';
import { MultiSelect } from '@/components/ui/multi-select';
import { buildOptions } from '@/utils/form'; import { buildOptions } from '@/utils/form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ParserFields } from '../../constant';
import { CommonProps } from './interface'; import { CommonProps } from './interface';
import { buildFieldNameWithPrefix } from './utils'; import { buildFieldNameWithPrefix } from './utils';
const options = buildOptions([ const options = buildOptions(ParserFields);
'from',
'to',
'cc',
'bcc',
'date',
'subject',
'body',
'attachments',
]);
export function EmailFormFields({ prefix }: CommonProps) { export function EmailFormFields({ prefix }: CommonProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -24,7 +16,14 @@ export function EmailFormFields({ prefix }: CommonProps) {
name={buildFieldNameWithPrefix(`fields`, prefix)} name={buildFieldNameWithPrefix(`fields`, prefix)}
label={t('dataflow.fields')} label={t('dataflow.fields')}
> >
<SelectWithSearch options={options}></SelectWithSearch> {(field) => (
<MultiSelect
options={options}
onValueChange={field.onChange}
defaultValue={field.value}
variant="inverted"
></MultiSelect>
)}
</RAGFlowFormItem> </RAGFlowFormItem>
</> </>
); );

View File

@ -17,14 +17,17 @@ import {
} from 'react-hook-form'; } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod'; import { z } from 'zod';
import { FileType, initialParserValues } from '../../constant'; import {
FileType,
InitialOutputFormatMap,
initialParserValues,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values'; import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change'; import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface'; import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list'; import { buildOutputList } from '../../utils/build-output-list';
import { Output } from '../components/output'; import { Output } from '../components/output';
import { OutputFormatFormField } from './common-form-fields'; import { OutputFormatFormField } from './common-form-fields';
import { InitialOutputFormatMap } from './constant';
import { EmailFormFields } from './email-form-fields'; import { EmailFormFields } from './email-form-fields';
import { ImageFormFields } from './image-form-fields'; import { ImageFormFields } from './image-form-fields';
import { PdfFormFields } from './pdf-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 { RAGFlowFormItem } from '@/components/ragflow-form';
import { SliderInputFormField } from '@/components/slider-input-form-field'; import { SliderInputFormField } from '@/components/slider-input-form-field';
import { Form } from '@/components/ui/form'; import { Form } from '@/components/ui/form';
@ -8,7 +9,11 @@ import { memo } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod'; import { z } from 'zod';
import { initialTokenizerValues, TokenizerSearchMethod } from '../../constant'; import {
initialTokenizerValues,
TokenizerFields,
TokenizerSearchMethod,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values'; import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change'; import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface'; import { INextOperatorForm } from '../../interface';
@ -21,11 +26,12 @@ const outputList = buildOutputList(initialTokenizerValues.outputs);
export const FormSchema = z.object({ export const FormSchema = z.object({
search_method: z.array(z.string()).min(1), search_method: z.array(z.string()).min(1),
filename_embd_weight: z.number(), filename_embd_weight: z.number(),
fields: z.string(),
}); });
const SearchMethodOptions = buildOptions(TokenizerSearchMethod); const SearchMethodOptions = buildOptions(TokenizerSearchMethod);
const FieldsOptions = [{ label: 'text', value: 'text' }]; const FieldsOptions = buildOptions(TokenizerFields);
const TokenizerForm = ({ node }: INextOperatorForm) => { const TokenizerForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -62,14 +68,7 @@ const TokenizerForm = ({ node }: INextOperatorForm) => {
step={0.01} step={0.01}
></SliderInputFormField> ></SliderInputFormField>
<RAGFlowFormItem name="fields" label={t('dataflow.fields')}> <RAGFlowFormItem name="fields" label={t('dataflow.fields')}>
{(field) => ( {(field) => <SelectWithSearch options={FieldsOptions} {...field} />}
<MultiSelect
options={FieldsOptions}
onValueChange={field.onChange}
defaultValue={field.value}
variant="inverted"
/>
)}
</RAGFlowFormItem> </RAGFlowFormItem>
</FormWrapper> </FormWrapper>
<div className="p-5"> <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 { Connection, Node, Position, ReactFlowInstance } from '@xyflow/react';
import humanId from 'human-id'; import humanId from 'human-id';
import { lowerFirst } from 'lodash'; import { lowerFirst } from 'lodash';
@ -8,6 +9,7 @@ import {
NodeMap, NodeMap,
Operator, Operator,
initialBeginValues, initialBeginValues,
initialContextValues,
initialHierarchicalMergerValues, initialHierarchicalMergerValues,
initialNoteValues, initialNoteValues,
initialParserValues, initialParserValues,
@ -21,6 +23,8 @@ import {
} from '../utils'; } from '../utils';
export const useInitializeOperatorParams = () => { export const useInitializeOperatorParams = () => {
const llmId = useFetchModelId();
const initialFormValuesMap = useMemo(() => { const initialFormValuesMap = useMemo(() => {
return { return {
[Operator.Begin]: initialBeginValues, [Operator.Begin]: initialBeginValues,
@ -29,8 +33,9 @@ export const useInitializeOperatorParams = () => {
[Operator.Tokenizer]: initialTokenizerValues, [Operator.Tokenizer]: initialTokenizerValues,
[Operator.Splitter]: initialSplitterValues, [Operator.Splitter]: initialSplitterValues,
[Operator.HierarchicalMerger]: initialHierarchicalMergerValues, [Operator.HierarchicalMerger]: initialHierarchicalMergerValues,
[Operator.Context]: { ...initialContextValues, llm_id: llmId },
}; };
}, []); }, [llmId]);
const initializeOperatorParams = useCallback( const initializeOperatorParams = useCallback(
(operatorName: Operator) => { (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 { useSendMessageBySSE } from '@/hooks/use-send-message';
import api from '@/utils/api'; import api from '@/utils/api';
import { get } from 'lodash'; import { get } from 'lodash';
import { useCallback, useState } from 'react'; import { useCallback, useContext } from 'react';
import { useParams } from 'umi'; import { useParams } from 'umi';
import { LogContext } from '../context';
import { useSaveGraphBeforeOpeningDebugDrawer } from './use-save-graph'; import { useSaveGraphBeforeOpeningDebugDrawer } from './use-save-graph';
export function useRunDataflow( export function useRunDataflow(
@ -11,7 +12,7 @@ export function useRunDataflow(
) { ) {
const { send } = useSendMessageBySSE(api.runCanvas); const { send } = useSendMessageBySSE(api.runCanvas);
const { id } = useParams(); const { id } = useParams();
const [messageId, setMessageId] = useState(); const { setMessageId } = useContext(LogContext);
const { handleRun: saveGraph, loading } = const { handleRun: saveGraph, loading } =
useSaveGraphBeforeOpeningDebugDrawer(showLogSheet!); useSaveGraphBeforeOpeningDebugDrawer(showLogSheet!);
@ -39,10 +40,10 @@ export function useRunDataflow(
return msgId; 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>; export type RunDataflowType = ReturnType<typeof useRunDataflow>;

View File

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

View File

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

View File

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

View File

@ -2,9 +2,10 @@ import { IconFont } from '@/components/icon-font';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
Blocks, Blocks,
File,
FileChartColumnIncreasing, FileChartColumnIncreasing,
FileStack,
Heading, Heading,
HousePlus,
ListMinus, ListMinus,
} from 'lucide-react'; } from 'lucide-react';
import { Operator } from './constant'; import { Operator } from './constant';
@ -15,15 +16,16 @@ interface IProps {
} }
export const OperatorIconMap = { export const OperatorIconMap = {
[Operator.Begin]: 'house-plus',
[Operator.Note]: 'notebook-pen', [Operator.Note]: 'notebook-pen',
}; };
export const SVGIconMap = { export const SVGIconMap = {
[Operator.Begin]: File,
[Operator.Parser]: FileChartColumnIncreasing, [Operator.Parser]: FileChartColumnIncreasing,
[Operator.Tokenizer]: ListMinus, [Operator.Tokenizer]: ListMinus,
[Operator.Splitter]: Blocks, [Operator.Splitter]: Blocks,
[Operator.HierarchicalMerger]: Heading, [Operator.HierarchicalMerger]: Heading,
[Operator.Context]: FileStack,
}; };
const Empty = () => { const Empty = () => {
@ -42,7 +44,7 @@ const OperatorIcon = ({ name, className }: IProps) => {
className, className,
)} )}
> >
<HousePlus className="rounded size-3" /> <File className="rounded size-3" />
</div> </div>
); );
} }
@ -50,7 +52,7 @@ const OperatorIcon = ({ name, className }: IProps) => {
return typeof Icon === 'string' ? ( return typeof Icon === 'string' ? (
<IconFont name={Icon} className={cn('size-5 ', className)}></IconFont> <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 { IAgentForm } from '@/interfaces/database/agent';
import { DSLComponents, RAGFlowNodeType } from '@/interfaces/database/flow'; import { DSLComponents, RAGFlowNodeType } from '@/interfaces/database/flow';
import { removeUselessFieldsFromValues } from '@/utils/form'; import { Edge, XYPosition } from '@xyflow/react';
import { Edge, Node, XYPosition } from '@xyflow/react';
import { FormInstance, FormListFieldData } from 'antd'; import { FormInstance, FormListFieldData } from 'antd';
import { humanId } from 'human-id'; import { humanId } from 'human-id';
import { curry, get, intersectionWith, isEmpty, isEqual, sample } from 'lodash'; import { curry, get, intersectionWith, isEmpty, isEqual, sample } from 'lodash';
@ -24,24 +23,13 @@ const buildComponentDownstreamOrUpstream = (
edges: Edge[], edges: Edge[],
nodeId: string, nodeId: string,
isBuildDownstream = true, isBuildDownstream = true,
nodes: Node[],
) => { ) => {
return edges return edges
.filter((y) => { .filter((y) => {
const node = nodes.find((x) => x.id === nodeId);
let isNotUpstreamTool = true; let isNotUpstreamTool = true;
let isNotUpstreamAgent = true; let isNotUpstreamAgent = true;
let isNotExceptionGoto = 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 ( return (
y[isBuildDownstream ? 'source' : 'target'] === nodeId && y[isBuildDownstream ? 'source' : 'target'] === nodeId &&
isNotUpstreamTool && isNotUpstreamTool &&
@ -54,9 +42,9 @@ const buildComponentDownstreamOrUpstream = (
const removeUselessDataInTheOperator = curry( const removeUselessDataInTheOperator = curry(
(operatorName: string, params: Record<string, unknown>) => { (operatorName: string, params: Record<string, unknown>) => {
if (operatorName === Operator.Categorize) { // if (operatorName === Operator.Categorize) {
return removeUselessFieldsFromValues(params, ''); // return removeUselessFieldsFromValues(params, '');
} // }
return params; return params;
}, },
); );
@ -197,8 +185,8 @@ export const buildDslComponentsByGraph = (
component_name: operatorName, component_name: operatorName,
params: buildOperatorParams(operatorName)(params) ?? {}, params: buildOperatorParams(operatorName)(params) ?? {},
}, },
downstream: buildComponentDownstreamOrUpstream(edges, id, true, nodes), downstream: buildComponentDownstreamOrUpstream(edges, id, true),
upstream: buildComponentDownstreamOrUpstream(edges, id, false, nodes), upstream: buildComponentDownstreamOrUpstream(edges, id, false),
parent_id: x?.parentId, parent_id: x?.parentId,
}; };
}); });
@ -352,25 +340,6 @@ export const generateNodeNamesWithIncreasingIndex = (
export const duplicateNodeForm = (nodeData?: RAGFlowNodeType['data']) => { export const duplicateNodeForm = (nodeData?: RAGFlowNodeType['data']) => {
const form: Record<string, any> = { ...(nodeData?.form ?? {}) }; 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 { return {
...(nodeData ?? { label: '' }), ...(nodeData ?? { label: '' }),
form, form,

View File

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

View File

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

View File

@ -1,46 +1,168 @@
import { Checkbox } from '@/components/ui/checkbox';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CheckedState } from '@radix-ui/react-checkbox';
import { useState } from 'react'; import { useState } from 'react';
interface FormatPreserveEditorProps { interface FormatPreserveEditorProps {
initialValue: string; initialValue: {
onSave: (value: string) => void; key: string;
type: string;
value: Array<{ [key: string]: string }>;
};
onSave: (value: any) => void;
className?: string; className?: string;
isSelect?: boolean;
isDelete?: boolean;
isChunck?: boolean;
handleCheckboxClick?: (id: string | number, checked: boolean) => void;
selectedChunkIds?: string[];
} }
const FormatPreserveEditor = ({ const FormatPreserveEditor = ({
initialValue, initialValue,
onSave, onSave,
className, className,
isChunck,
handleCheckboxClick,
selectedChunkIds,
}: FormatPreserveEditorProps) => { }: FormatPreserveEditorProps) => {
const [content, setContent] = useState(initialValue); const [content, setContent] = useState(initialValue);
const [isEditing, setIsEditing] = useState(false); // const [isEditing, setIsEditing] = useState(false);
const [activeEditIndex, setActiveEditIndex] = useState<number | undefined>(
const handleEdit = () => setIsEditing(true); undefined,
);
const handleSave = () => { console.log('initialValue', initialValue);
onSave(content); const handleEdit = (e?: any, index?: number) => {
setIsEditing(false); 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 ( return (
<div className="editor-container"> <div className="editor-container">
{isEditing ? ( {/* {isEditing && content.key === 'json' ? (
<Textarea <Textarea
className={cn( 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, className,
)} )}
value={content} value={content.value}
onChange={(e) => setContent(e.target.value)} onChange={handleChange}
onBlur={handleSave} onBlur={handleSave}
autoSize={{ maxRows: 100 }} autoSize={{ maxRows: 100 }}
autoFocus autoFocus
/> />
) : ( ) : (
<pre className="text-text-secondary" onClick={handleEdit}> <>
{content} {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> </pre>
)} )}
</>
)}*/}
</div> </div>
); );
}; };

View File

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

View File

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

View File

@ -2,3 +2,14 @@ export enum ChunkTextMode {
Full = 'full', Full = 'full',
Ellipse = 'ellipse', 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 { import {
useCreateChunk, useCreateChunk,
useDeleteChunk, useDeleteChunk,
@ -8,40 +8,17 @@ import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { IChunk } from '@/interfaces/database/knowledge'; import { IChunk } from '@/interfaces/database/knowledge';
import kbService from '@/services/knowledge-service'; import kbService from '@/services/knowledge-service';
import { formatSecondsToHumanReadable } from '@/utils/date';
import { buildChunkHighlights } from '@/utils/document-util'; 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 { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IHighlight } from 'react-pdf-highlighter'; import { IHighlight } from 'react-pdf-highlighter';
import { useParams, useSearchParams } from 'umi'; 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?: { export const useFetchPipelineFileLogDetail = (props?: {
isEdit?: boolean; isEdit?: boolean;
refreshCount?: number; refreshCount?: number;
@ -199,52 +176,105 @@ export const useFetchParserList = () => {
}; };
}; };
export const useRerunDataflow = () => { export const useRerunDataflow = ({
data,
}: {
data: IPipelineFileLogDetail;
}) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isChange, setIsChange] = 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 { return {
loading, loading,
setLoading, setLoading,
isChange, isChange,
setIsChange, setIsChange,
handleReRunFunc,
}; };
}; };
export const useFetchPaserText = () => { export const useTimelineDataFlow = (data: IPipelineFileLogDetail) => {
const initialText = const timelineNodes: TimelineNode[] = useMemo(() => {
'第一行文本\n\t第二行缩进文本\n第三行 多个空格 第一行文本\n\t第二行缩进文本\n第三行 ' + const nodes: Array<ITimelineNodeObj & { id: number | string }> = [];
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + console.log('time-->', data);
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + const times = data?.dsl?.components;
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + if (times) {
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + const getNode = (
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' + key: string,
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格'; index: number,
const [loading, setLoading] = useState(false); type:
const [data, setData] = useState<string>(initialText); | TimelineNodeType.begin
const { t } = useTranslation(); | TimelineNodeType.parser
const queryClient = useQueryClient(); | TimelineNodeType.splitter
| TimelineNodeType.tokenizer
| TimelineNodeType.characterSplitter
| TimelineNodeType.titleSplitter,
) => {
const node = times[key].obj;
const name = camelCase(
node.component_name,
) as keyof typeof TimelineNodeObj;
const { let tempType = type;
// data, if (name === TimelineNodeType.parser) {
// isPending: loading, tempType = TimelineNodeType.parser;
mutateAsync, } else if (name === TimelineNodeType.tokenizer) {
} = useMutation({ tempType = TimelineNodeType.tokenizer;
mutationKey: ['createChunk'], } else if (
mutationFn: async (payload: any) => { name === TimelineNodeType.characterSplitter ||
// let service = kbService.create_chunk; name === TimelineNodeType.titleSplitter ||
// if (payload.chunk_id) { name === TimelineNodeType.splitter
// service = kbService.set_chunk; ) {
// } tempType = TimelineNodeType.splitter;
// const { data } = await service(payload); }
// if (data.code === 0) { const timeNode = {
message.success(t('message.created')); ...TimelineNodeObj[name],
setTimeout(() => { clickable: true,
queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] }); id: index,
}, 1000); // Delay to ensure the list is updated className: 'w-32',
// } completed: false,
// return data?.code; 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, useGetChunkHighlights,
useHandleChunkCardClick, useHandleChunkCardClick,
useRerunDataflow, useRerunDataflow,
useTimelineDataFlow,
} from './hooks'; } from './hooks';
import DocumentHeader from './components/document-preview/document-header'; import DocumentHeader from './components/document-preview/document-header';
@ -29,13 +30,11 @@ import {
useNavigatePage, useNavigatePage,
} from '@/hooks/logic-hooks/navigate-hooks'; } from '@/hooks/logic-hooks/navigate-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { ChunkerContainer } from './chunker';
import { useGetDocumentUrl } from './components/document-preview/hooks'; import { useGetDocumentUrl } from './components/document-preview/hooks';
import TimelineDataFlow, { import TimelineDataFlow from './components/time-line';
TimelineNodeArr, import { TimelineNodeType } from './constant';
TimelineNodeType,
} from './components/time-line';
import styles from './index.less'; import styles from './index.less';
import { IDslComponent } from './interface';
import ParserContainer from './parser'; import ParserContainer from './parser';
const Chunk = () => { const Chunk = () => {
@ -45,9 +44,10 @@ const Chunk = () => {
const { selectedChunkId } = useHandleChunkCardClick(); const { selectedChunkId } = useHandleChunkCardClick();
const [activeStepId, setActiveStepId] = useState<number | string>(0); const [activeStepId, setActiveStepId] = useState<number | string>(0);
const { data: dataset } = useFetchPipelineFileLogDetail(); const { data: dataset } = useFetchPipelineFileLogDetail();
const { isChange, setIsChange } = useRerunDataflow();
const { t } = useTranslation(); const { t } = useTranslation();
const { timelineNodes } = useTimelineDataFlow(dataset);
const { navigateToDataset, getQueryString, navigateToDatasetList } = const { navigateToDataset, getQueryString, navigateToDatasetList } =
useNavigatePage(); useNavigatePage();
const fileUrl = useGetDocumentUrl(); const fileUrl = useGetDocumentUrl();
@ -69,8 +69,15 @@ const Chunk = () => {
return 'unknown'; return 'unknown';
}, [documentInfo]); }, [documentInfo]);
const {
handleReRunFunc,
isChange,
setIsChange,
loading: reRunLoading,
} = useRerunDataflow({
data: dataset,
});
const handleStepChange = (id: number | string, step: TimelineNode) => { const handleStepChange = (id: number | string, step: TimelineNode) => {
console.log(id, step);
if (isChange) { if (isChange) {
Modal.show({ Modal.show({
visible: true, visible: true,
@ -114,12 +121,14 @@ const Chunk = () => {
}; };
const { type } = useGetKnowledgeSearchParams(); const { type } = useGetKnowledgeSearchParams();
const currentTimeNode: TimelineNode = useMemo(() => { const currentTimeNode: TimelineNode = useMemo(() => {
return ( return (
TimelineNodeArr.find((node) => node.id === activeStepId) || timelineNodes.find((node) => node.id === activeStepId) ||
({} as TimelineNode) ({} as TimelineNode)
); );
}, [activeStepId]); }, [activeStepId, timelineNodes]);
return ( return (
<> <>
<PageHeader> <PageHeader>
@ -134,10 +143,10 @@ const Chunk = () => {
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink <BreadcrumbLink
onClick={navigateToDataset( onClick={navigateToDataset(
getQueryString(QueryStringMap.id) as string, getQueryString(QueryStringMap.KnowledgeId) as string,
)} )}
> >
{dataset.name} {t('knowledgeDetails.overview')}
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
@ -152,6 +161,8 @@ const Chunk = () => {
<TimelineDataFlow <TimelineDataFlow
activeFunc={handleStepChange} activeFunc={handleStepChange}
activeId={activeStepId} activeId={activeStepId}
data={dataset}
timelineNodes={timelineNodes}
/> />
</div> </div>
)} )}
@ -173,20 +184,31 @@ const Chunk = () => {
</div> </div>
<div className="h-dvh border-r -mt-3"></div> <div className="h-dvh border-r -mt-3"></div>
<div className="w-3/5 h-full"> <div className="w-3/5 h-full">
{currentTimeNode?.type === TimelineNodeType.chunk && ( {/* {currentTimeNode?.type === TimelineNodeType.splitter && (
<ChunkerContainer <ChunkerContainer
isChange={isChange} isChange={isChange}
setIsChange={setIsChange} setIsChange={setIsChange}
step={currentTimeNode as TimelineNode} step={currentTimeNode as TimelineNode}
/> />
)} )} */}
{currentTimeNode?.type === TimelineNodeType.parser && ( {/* {currentTimeNode?.type === TimelineNodeType.parser && ( */}
{(currentTimeNode?.type === TimelineNodeType.parser ||
currentTimeNode?.type === TimelineNodeType.splitter) && (
<ParserContainer <ParserContainer
isChange={isChange} isChange={isChange}
reRunLoading={reRunLoading}
setIsChange={setIsChange} setIsChange={setIsChange}
step={currentTimeNode as TimelineNode} step={currentTimeNode as TimelineNode}
data={
currentTimeNode.detail as {
value: IDslComponent;
key: string;
}
}
reRunFunc={handleReRunFunc}
/> />
)} )}
{/* )} */}
<Spotlight opcity={0.6} coverage={60} /> <Spotlight opcity={0.6} coverage={60} />
</div> </div>
</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 Spotlight from '@/components/spotlight';
import { Spin } from '@/components/ui/spin'; import { Spin } from '@/components/ui/spin';
import classNames from 'classnames'; import classNames from 'classnames';
import { useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; 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 RerunButton from './components/rerun-button';
import { useFetchParserList, useFetchPaserText } from './hooks'; import { TimelineNodeType } from './constant';
import { useFetchParserList } from './hooks';
import { IDslComponent } from './interface';
interface IProps { interface IProps {
isChange: boolean; isChange: boolean;
setIsChange: (isChange: boolean) => void; setIsChange: (isChange: boolean) => void;
step?: TimelineNode; step?: TimelineNode;
data: { value: IDslComponent; key: string };
reRunLoading: boolean;
reRunFunc: (data: { value: IDslComponent; key: string }) => void;
} }
const ParserContainer = (props: IProps) => { const ParserContainer = (props: IProps) => {
const { isChange, setIsChange, step } = props; const { isChange, setIsChange, step, data, reRunFunc, reRunLoading } = props;
const { data: initialValue, rerun: onSave } = useFetchPaserText();
const { t } = useTranslation(); const { t } = useTranslation();
const { loading } = useFetchParserList(); 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 [initialText, setInitialText] = useState(initialValue);
const handleSave = (newContent: string) => { const handleSave = (newContent: any) => {
console.log('保存内容:', newContent); console.log('newContent-change-->', newContent, initialValue);
if (newContent !== initialText) { if (JSON.stringify(newContent) !== JSON.stringify(initialValue)) {
setIsChange(true); setIsChange(true);
onSave(newContent); setInitialText(newContent);
} else { } else {
setIsChange(false); setIsChange(false);
} }
// Here, the API is called to send newContent to the backend // Here, the API is called to send newContent to the backend
}; };
const handleReRunFunc = () => {
setIsChange(false); 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 ( return (
<> <>
{isChange && ( {isChange && (
<div className=" absolute top-2 right-6"> <div className=" absolute top-2 right-6">
<RerunButton step={step} onRerun={handleReRunFunc} /> <RerunButton
step={step}
onRerun={handleReRunFunc}
loading={reRunLoading}
/>
</div> </div>
)} )}
<div className={classNames('flex flex-col w-full')}> <div className={classNames('flex flex-col w-full')}>
@ -51,11 +131,33 @@ const ParserContainer = (props: IProps) => {
</div> </div>
</div> </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} initialValue={initialText}
onSave={handleSave} 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} /> <Spotlight opcity={0.6} coverage={60} />
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,7 @@ export default {
// data pipeline log // data pipeline log
fetchDataPipelineLog: `${api_host}/kb/list_pipeline_logs`, fetchDataPipelineLog: `${api_host}/kb/list_pipeline_logs`,
get_pipeline_detail: `${api_host}/kb/pipeline_log_detail`, get_pipeline_detail: `${api_host}/kb/pipeline_log_detail`,
fetchPipelineDatasetLogs: `${api_host}/kb/list_pipeline_dataset_logs`,
// tags // tags
listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`, listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`,
@ -169,6 +170,7 @@ export default {
fetchExternalAgentInputs: (canvasId: string) => fetchExternalAgentInputs: (canvasId: string) =>
`${ExternalApi}${api_host}/agentbots/${canvasId}/inputs`, `${ExternalApi}${api_host}/agentbots/${canvasId}/inputs`,
prompt: `${api_host}/canvas/prompts`, prompt: `${api_host}/canvas/prompts`,
cancelDataflow: (id: string) => `${api_host}/canvas/cancel/${id}`,
// mcp server // mcp server
listMcpServer: `${api_host}/mcp_server/list`, listMcpServer: `${api_host}/mcp_server/list`,

View File

@ -1,4 +1,5 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { toFixed } from './common-util';
export function formatDate(date: any) { export function formatDate(date: any) {
if (!date) { if (!date) {
@ -43,3 +44,20 @@ export function formatStandardDate(date: any) {
} }
return parsedDate.format('YYYY-MM-DD'); 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: { extend: {
colors: { colors: {
border: 'var(--colors-outline-neutral-strong)', border: 'var(--border-default)',
input: 'hsl(var(--input))', input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))', ring: 'hsl(var(--ring))',
background: 'var(--background)', background: 'var(--background)',

View File

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