Feat: Display the pipeline on the agent canvas #9869 (#10638)

### What problem does this PR solve?

Feat: Display the pipeline on the agent canvas #9869
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-10-17 18:47:33 +08:00
committed by GitHub
parent c9e56d20cf
commit 685114d253
48 changed files with 2636 additions and 423 deletions

View File

@ -53,6 +53,10 @@ export enum AgentCategory {
DataflowCanvas = 'dataflow_canvas',
}
export enum AgentQuery {
Category = 'category',
}
export enum DataflowOperator {
Begin = 'File',
Note = 'Note',
@ -62,3 +66,55 @@ export enum DataflowOperator {
HierarchicalMerger = 'HierarchicalMerger',
Extractor = 'Extractor',
}
export enum Operator {
Begin = 'Begin',
Retrieval = 'Retrieval',
Categorize = 'Categorize',
Message = 'Message',
Relevant = 'Relevant',
RewriteQuestion = 'RewriteQuestion',
KeywordExtract = 'KeywordExtract',
Baidu = 'Baidu',
DuckDuckGo = 'DuckDuckGo',
Wikipedia = 'Wikipedia',
PubMed = 'PubMed',
ArXiv = 'ArXiv',
Google = 'Google',
Bing = 'Bing',
GoogleScholar = 'GoogleScholar',
DeepL = 'DeepL',
GitHub = 'GitHub',
BaiduFanyi = 'BaiduFanyi',
QWeather = 'QWeather',
ExeSQL = 'ExeSQL',
Switch = 'Switch',
WenCai = 'WenCai',
AkShare = 'AkShare',
YahooFinance = 'YahooFinance',
Jin10 = 'Jin10',
TuShare = 'TuShare',
Note = 'Note',
Crawler = 'Crawler',
Invoke = 'Invoke',
Email = 'Email',
Iteration = 'Iteration',
IterationStart = 'IterationItem',
Code = 'CodeExec',
WaitingDialogue = 'WaitingDialogue',
Agent = 'Agent',
Tool = 'Tool',
TavilySearch = 'TavilySearch',
TavilyExtract = 'TavilyExtract',
UserFillUp = 'UserFillUp',
StringTransform = 'StringTransform',
SearXNG = 'SearXNG',
Placeholder = 'Placeholder',
File = 'File', // pipeline
Parser = 'Parser',
Tokenizer = 'Tokenizer',
Splitter = 'Splitter',
HierarchicalMerger = 'HierarchicalMerger',
Extractor = 'Extractor',
Generate = 'Generate',
}

View File

@ -1,3 +1,4 @@
import { AgentCategory, AgentQuery } from '@/constants/agent';
import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface';
import { Routes } from '@/routes';
import { useCallback } from 'react';
@ -70,8 +71,8 @@ export const useNavigatePage = () => {
}, [navigate]);
const navigateToAgent = useCallback(
(id: string) => () => {
navigate(`${Routes.Agent}/${id}`);
(id: string, category?: AgentCategory) => () => {
navigate(`${Routes.Agent}/${id}?${AgentQuery.Category}=${category}`);
},
[navigate],
);

View File

@ -56,19 +56,24 @@ import { RagNode } from './node';
import { AgentNode } from './node/agent-node';
import { BeginNode } from './node/begin-node';
import { CategorizeNode } from './node/categorize-node';
import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown';
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { ExtractorNode } from './node/extractor-node';
import { FileNode } from './node/file-node';
import { GenerateNode } from './node/generate-node';
import { InvokeNode } from './node/invoke-node';
import { IterationNode, IterationStartNode } from './node/iteration-node';
import { KeywordNode } from './node/keyword-node';
import { MessageNode } from './node/message-node';
import NoteNode from './node/note-node';
import ParserNode from './node/parser-node';
import { PlaceholderNode } from './node/placeholder-node';
import { RelevantNode } from './node/relevant-node';
import { RetrievalNode } from './node/retrieval-node';
import { RewriteNode } from './node/rewrite-node';
import { SplitterNode } from './node/splitter-node';
import { SwitchNode } from './node/switch-node';
import { TemplateNode } from './node/template-node';
import TokenizerNode from './node/tokenizer-node';
import { ToolNode } from './node/tool-node';
export const nodeTypes: NodeTypes = {
@ -91,6 +96,11 @@ export const nodeTypes: NodeTypes = {
iterationStartNode: IterationStartNode,
agentNode: AgentNode,
toolNode: ToolNode,
fileNode: FileNode,
parserNode: ParserNode,
tokenizerNode: TokenizerNode,
splitterNode: SplitterNode,
contextNode: ExtractorNode,
};
const edgeTypes = {
@ -194,6 +204,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
getConnectionStartContext,
shouldPreventClose,
onMove,
nodeId,
} = useConnectionDrag(
reactFlowInstance,
originalOnConnect,
@ -312,7 +323,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
}
}
>
<InnerNextStepDropdown
<NextStepDropdown
hideModal={() => {
removePlaceholderNode();
hideModal();
@ -320,9 +331,10 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
}}
position={dropdownPosition}
onNodeCreated={onNodeCreated}
nodeId={nodeId}
>
<span></span>
</InnerNextStepDropdown>
</NextStepDropdown>
</HandleContext.Provider>
)}
</AgentInstanceContext.Provider>

View File

@ -17,6 +17,9 @@ import {
SelectValue,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { PropsWithChildren } from 'react';
export function CardWithForm() {
return (
<Card className="w-[350px]">
@ -55,3 +58,13 @@ export function CardWithForm() {
</Card>
);
}
type LabelCardProps = {
className?: string;
} & PropsWithChildren;
export function LabelCard({ children, className }: LabelCardProps) {
return (
<div className={cn('bg-bg-card rounded-sm p-1', className)}>{children}</div>
);
}

View File

@ -0,0 +1,196 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import { Operator } from '@/constants/agent';
import useGraphStore from '@/pages/agent/store';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { OperatorItemList } from './operator-item-list';
export function AccordionOperators({
isCustomDropdown = false,
mousePosition,
}: {
isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number };
}) {
const { t } = useTranslation();
return (
<Accordion
type="multiple"
className="px-2 text-text-title max-h-[45vh] overflow-auto scrollbar-none"
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
>
<AccordionItem value="item-1">
<AccordionTrigger className="text-xl">
{t('flow.foundation')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[Operator.Agent, Operator.Retrieval]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger className="text-xl">
{t('flow.dialog')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[Operator.Message, Operator.UserFillUp]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger className="text-xl">
{t('flow.flow')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[
Operator.Switch,
Operator.Iteration,
Operator.Categorize,
]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-4">
<AccordionTrigger className="text-xl">
{t('flow.dataManipulation')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[Operator.Code, Operator.StringTransform]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-5">
<AccordionTrigger className="text-xl">
{t('flow.tools')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[
Operator.TavilySearch,
Operator.TavilyExtract,
Operator.ExeSQL,
Operator.Google,
Operator.YahooFinance,
Operator.Email,
Operator.DuckDuckGo,
Operator.Wikipedia,
Operator.GoogleScholar,
Operator.ArXiv,
Operator.PubMed,
Operator.GitHub,
Operator.Invoke,
Operator.WenCai,
Operator.SearXNG,
]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
// Limit the number of operators of a certain type on the canvas to only one
function useRestrictSingleOperatorOnCanvas() {
const { findNodeByName } = useGraphStore((state) => state);
const restrictSingleOperatorOnCanvas = useCallback(
(singleOperators: Operator[]) => {
const list: Operator[] = [];
singleOperators.forEach((operator) => {
if (!findNodeByName(operator)) {
list.push(operator);
}
});
return list;
},
[findNodeByName],
);
return restrictSingleOperatorOnCanvas;
}
export function PipelineAccordionOperators({
isCustomDropdown = false,
mousePosition,
nodeId,
}: {
isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number };
nodeId?: string;
}) {
const restrictSingleOperatorOnCanvas = useRestrictSingleOperatorOnCanvas();
const { getOperatorTypeFromId } = useGraphStore((state) => state);
const operators = useMemo(() => {
let list = [
...restrictSingleOperatorOnCanvas([Operator.Parser, Operator.Tokenizer]),
];
list.push(Operator.Extractor);
return list;
}, [restrictSingleOperatorOnCanvas]);
const chunkerOperators = useMemo(() => {
return [
...restrictSingleOperatorOnCanvas([
Operator.Splitter,
Operator.HierarchicalMerger,
]),
];
}, [restrictSingleOperatorOnCanvas]);
const showChunker = useMemo(() => {
return (
getOperatorTypeFromId(nodeId) !== Operator.Extractor &&
chunkerOperators.length > 0
);
}, [chunkerOperators.length, getOperatorTypeFromId, nodeId]);
return (
<>
<OperatorItemList
operators={operators}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
{showChunker && (
<Accordion
type="single"
collapsible
className="w-full px-4"
defaultValue="item-1"
>
<AccordionItem value="item-1">
<AccordionTrigger>Chunker</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={chunkerOperators}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
</>
);
}

View File

@ -1,230 +1,33 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { IModalProps } from '@/interfaces/common';
import { Operator } from '@/pages/agent/constant';
import { AgentInstanceContext, HandleContext } from '@/pages/agent/context';
import OperatorIcon from '@/pages/agent/operator-icon';
import { Position } from '@xyflow/react';
import { useIsPipeline } from '@/pages/agent/hooks/use-is-pipeline';
import { t } from 'i18next';
import { lowerFirst } from 'lodash';
import { PropsWithChildren, memo, useEffect, useRef } from 'react';
import {
PropsWithChildren,
createContext,
memo,
useContext,
useEffect,
useRef,
} from 'react';
import { useTranslation } from 'react-i18next';
type OperatorItemProps = {
operators: Operator[];
isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number };
};
const HideModalContext = createContext<IModalProps<any>['showModal']>(() => {});
const OnNodeCreatedContext = createContext<
((newNodeId: string) => void) | undefined
>(undefined);
function OperatorItemList({
operators,
isCustomDropdown = false,
mousePosition,
}: OperatorItemProps) {
const { addCanvasNode } = useContext(AgentInstanceContext);
const handleContext = useContext(HandleContext);
const hideModal = useContext(HideModalContext);
const onNodeCreated = useContext(OnNodeCreatedContext);
const { t } = useTranslation();
const handleClick =
(operator: Operator): React.MouseEventHandler<HTMLElement> =>
(e) => {
const contextData = handleContext || {
nodeId: '',
id: '',
type: 'source' as const,
position: Position.Right,
isFromConnectionDrag: true,
};
const mockEvent = mousePosition
? {
clientX: mousePosition.x,
clientY: mousePosition.y,
}
: e;
const newNodeId = addCanvasNode(operator, contextData)(mockEvent);
if (onNodeCreated && newNodeId) {
onNodeCreated(newNodeId);
}
hideModal?.();
};
const renderOperatorItem = (operator: Operator) => {
const commonContent = (
<div className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start">
<OperatorIcon name={operator} />
{t(`flow.${lowerFirst(operator)}`)}
</div>
);
return (
<Tooltip key={operator}>
<TooltipTrigger asChild>
{isCustomDropdown ? (
<li onClick={handleClick(operator)}>{commonContent}</li>
) : (
<DropdownMenuItem
key={operator}
className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start"
onClick={handleClick(operator)}
onSelect={() => hideModal?.()}
>
<OperatorIcon name={operator} />
{t(`flow.${lowerFirst(operator)}`)}
</DropdownMenuItem>
)}
</TooltipTrigger>
<TooltipContent side="right" sideOffset={24}>
<p>{t(`flow.${lowerFirst(operator)}Description`)}</p>
</TooltipContent>
</Tooltip>
);
};
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
}
function AccordionOperators({
isCustomDropdown = false,
mousePosition,
}: {
isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number };
}) {
return (
<Accordion
type="multiple"
className="px-2 text-text-title max-h-[45vh] overflow-auto scrollbar-none"
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
>
<AccordionItem value="item-1">
<AccordionTrigger className="text-xl">
{t('flow.foundation')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[Operator.Agent, Operator.Retrieval]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger className="text-xl">
{t('flow.dialog')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[Operator.Message, Operator.UserFillUp]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger className="text-xl">
{t('flow.flow')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[
Operator.Switch,
Operator.Iteration,
Operator.Categorize,
]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-4">
<AccordionTrigger className="text-xl">
{t('flow.dataManipulation')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[Operator.Code, Operator.StringTransform]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-5">
<AccordionTrigger className="text-xl">
{t('flow.tools')}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={[
Operator.TavilySearch,
Operator.TavilyExtract,
Operator.ExeSQL,
Operator.Google,
Operator.YahooFinance,
Operator.Email,
Operator.DuckDuckGo,
Operator.Wikipedia,
Operator.GoogleScholar,
Operator.ArXiv,
Operator.PubMed,
Operator.GitHub,
Operator.Invoke,
Operator.WenCai,
Operator.SearXNG,
]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
AccordionOperators,
PipelineAccordionOperators,
} from './accordion-operators';
import { HideModalContext, OnNodeCreatedContext } from './operator-item-list';
export function InnerNextStepDropdown({
children,
hideModal,
position,
onNodeCreated,
nodeId,
}: PropsWithChildren &
IModalProps<any> & {
position?: { x: number; y: number };
onNodeCreated?: (newNodeId: string) => void;
nodeId?: string;
}) {
const dropdownRef = useRef<HTMLDivElement>(null);
const isPipeline = useIsPipeline();
useEffect(() => {
if (position && hideModal) {
@ -260,10 +63,18 @@ export function InnerNextStepDropdown({
</div>
<HideModalContext.Provider value={hideModal}>
<OnNodeCreatedContext.Provider value={onNodeCreated}>
<AccordionOperators
isCustomDropdown={true}
mousePosition={position}
></AccordionOperators>
{isPipeline ? (
<PipelineAccordionOperators
isCustomDropdown={true}
mousePosition={position}
nodeId={nodeId}
></PipelineAccordionOperators>
) : (
<AccordionOperators
isCustomDropdown={true}
mousePosition={position}
></AccordionOperators>
)}
</OnNodeCreatedContext.Provider>
</HideModalContext.Provider>
</div>
@ -287,7 +98,11 @@ export function InnerNextStepDropdown({
>
<DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel>
<HideModalContext.Provider value={hideModal}>
<AccordionOperators></AccordionOperators>
{isPipeline ? (
<PipelineAccordionOperators></PipelineAccordionOperators>
) : (
<AccordionOperators></AccordionOperators>
)}
</HideModalContext.Provider>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -0,0 +1,100 @@
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Operator } from '@/constants/agent';
import { IModalProps } from '@/interfaces/common';
import { AgentInstanceContext, HandleContext } from '@/pages/agent/context';
import OperatorIcon from '@/pages/agent/operator-icon';
import { Position } from '@xyflow/react';
import { lowerFirst } from 'lodash';
import { createContext, useContext } from 'react';
import { useTranslation } from 'react-i18next';
export type OperatorItemProps = {
operators: Operator[];
isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number };
};
export const HideModalContext = createContext<IModalProps<any>['showModal']>(
() => {},
);
export const OnNodeCreatedContext = createContext<
((newNodeId: string) => void) | undefined
>(undefined);
export function OperatorItemList({
operators,
isCustomDropdown = false,
mousePosition,
}: OperatorItemProps) {
const { addCanvasNode } = useContext(AgentInstanceContext);
const handleContext = useContext(HandleContext);
const hideModal = useContext(HideModalContext);
const onNodeCreated = useContext(OnNodeCreatedContext);
const { t } = useTranslation();
const handleClick =
(operator: Operator): React.MouseEventHandler<HTMLElement> =>
(e) => {
const contextData = handleContext || {
nodeId: '',
id: '',
type: 'source' as const,
position: Position.Right,
isFromConnectionDrag: true,
};
const mockEvent = mousePosition
? {
clientX: mousePosition.x,
clientY: mousePosition.y,
}
: e;
const newNodeId = addCanvasNode(operator, contextData)(mockEvent);
if (onNodeCreated && newNodeId) {
onNodeCreated(newNodeId);
}
hideModal?.();
};
const renderOperatorItem = (operator: Operator) => {
const commonContent = (
<div className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start">
<OperatorIcon name={operator} />
{t(`flow.${lowerFirst(operator)}`)}
</div>
);
return (
<Tooltip key={operator}>
<TooltipTrigger asChild>
{isCustomDropdown ? (
<li onClick={handleClick(operator)}>{commonContent}</li>
) : (
<DropdownMenuItem
key={operator}
className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start"
onClick={handleClick(operator)}
onSelect={() => hideModal?.()}
>
<OperatorIcon name={operator} />
{t(`flow.${lowerFirst(operator)}`)}
</DropdownMenuItem>
)}
</TooltipTrigger>
<TooltipContent side="right" sideOffset={24}>
<p>{t(`flow.${lowerFirst(operator)}Description`)}</p>
</TooltipContent>
</Tooltip>
);
};
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
}

View File

@ -0,0 +1,18 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { IRagNode } from '@/interfaces/database/agent';
import { NodeProps } from '@xyflow/react';
import { get } from 'lodash';
import { LabelCard } from './card';
import { RagNode } from './index';
export function ExtractorNode({ ...props }: NodeProps<IRagNode>) {
const { data } = props;
return (
<RagNode {...props}>
<LabelCard>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</LabelCard>
</RagNode>
);
}

View File

@ -0,0 +1,62 @@
import { IBeginNode } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { NodeProps, Position } from '@xyflow/react';
import get from 'lodash/get';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
BeginQueryType,
BeginQueryTypeIconMap,
NodeHandleId,
Operator,
} from '../../constant';
import { BeginQuery } from '../../interface';
import OperatorIcon from '../../operator-icon';
import { CommonHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import styles from './index.less';
import { NodeWrapper } from './node-wrapper';
// TODO: do not allow other nodes to connect to this node
function InnerFileNode({ data, id, selected }: NodeProps<IBeginNode>) {
const { t } = useTranslation();
const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {});
return (
<NodeWrapper selected={selected}>
<CommonHandle
type="source"
position={Position.Right}
isConnectable
style={RightHandleStyle}
nodeId={id}
id={NodeHandleId.Start}
></CommonHandle>
<section className="flex items-center gap-2">
<OperatorIcon name={data.label as Operator}></OperatorIcon>
<div className="truncate text-center font-semibold text-sm">
{t(`dataflow.begin`)}
</div>
</section>
<section className={cn(styles.generateParameters, 'flex gap-2 flex-col')}>
{Object.entries(inputs).map(([key, val], idx) => {
const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType];
return (
<div
key={idx}
className={cn(styles.conditionBlock, 'flex gap-1.5 items-center')}
>
<Icon className="size-4" />
<label htmlFor="">{key}</label>
<span className={styles.parameterValue}>{val.name}</span>
<span className="flex-1">{val.optional ? 'Yes' : 'No'}</span>
</div>
);
})}
</section>
</NodeWrapper>
);
}
export const FileNode = memo(InnerFileNode);

View File

@ -6,7 +6,7 @@ import { useMemo } from 'react';
import { NodeHandleId } from '../../constant';
import { HandleContext } from '../../context';
import { useDropdownManager } from '../context';
import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
import { NextStepDropdown } from './dropdown/next-step-dropdown';
export function CommonHandle({
className,
@ -50,14 +50,15 @@ export function CommonHandle({
>
<Plus className="size-3 pointer-events-none text-white hidden group-hover:inline-block" />
{visible && (
<InnerNextStepDropdown
<NextStepDropdown
nodeId={nodeId}
hideModal={() => {
hideModal();
clearActiveDropdown();
}}
>
<span></span>
</InnerNextStepDropdown>
</NextStepDropdown>
)}
</Handle>
</HandleContext.Provider>

View File

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

View File

@ -9,6 +9,7 @@ interface IProps {
gap?: number;
className?: string;
wrapperClassName?: string;
icon?: React.ReactNode;
}
const InnerNodeHeader = ({
@ -16,11 +17,12 @@ const InnerNodeHeader = ({
name,
className,
wrapperClassName,
icon,
}: IProps) => {
return (
<section className={cn(wrapperClassName, 'pb-2')}>
<div className={cn(className, 'flex gap-2.5')}>
<OperatorIcon name={label as Operator}></OperatorIcon>
{icon || <OperatorIcon name={label as Operator}></OperatorIcon>}
<span className="truncate text-center font-semibold text-sm">
{name}
</span>

View File

@ -0,0 +1,57 @@
import { NodeCollapsible } from '@/components/collapse';
import { BaseNode } from '@/interfaces/database/agent';
import { NodeProps, Position } from '@xyflow/react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { NodeHandleId } from '../../constant';
import { ParserFormSchemaType } from '../../form/parser-form';
import { LabelCard } from './card';
import { CommonHandle } from './handle';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
function ParserNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<BaseNode<ParserFormSchemaType>>) {
const { t } = useTranslation();
return (
<NodeWrapper selected={selected}>
<CommonHandle
id={NodeHandleId.End}
type="target"
position={Position.Left}
isConnectable={isConnectable}
style={LeftHandleStyle}
nodeId={id}
></CommonHandle>
<CommonHandle
type="source"
position={Position.Right}
isConnectable={isConnectable}
id={NodeHandleId.Start}
style={RightHandleStyle}
nodeId={id}
isConnectableEnd={false}
></CommonHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<NodeCollapsible items={data.form?.setups}>
{(x, idx) => (
<LabelCard
key={idx}
className="flex flex-col text-text-primary gap-1"
>
<span className="text-text-secondary">Parser {idx + 1}</span>
{t(`dataflow.fileFormatOptions.${x.fileFormat}`)}
</LabelCard>
)}
</NodeCollapsible>
</NodeWrapper>
);
}
export default memo(ParserNode);

View File

@ -0,0 +1,58 @@
import { IRagNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react';
import { PropsWithChildren, memo } from 'react';
import { NodeHandleId, Operator } from '../../constant';
import OperatorIcon from '../../operator-icon';
import { LabelCard } from './card';
import { CommonHandle } from './handle';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar';
type RagNodeProps = NodeProps<IRagNode> & PropsWithChildren;
function InnerSplitterNode({
id,
data,
isConnectable = true,
selected,
}: RagNodeProps) {
return (
<ToolBar
selected={selected}
id={id}
label={data.label}
showCopy={false}
showRun={false}
>
<NodeWrapper selected={selected}>
<CommonHandle
id={NodeHandleId.End}
type="target"
position={Position.Left}
isConnectable={isConnectable}
style={LeftHandleStyle}
nodeId={id}
></CommonHandle>
<CommonHandle
type="source"
position={Position.Right}
isConnectable={isConnectable}
id={NodeHandleId.Start}
style={RightHandleStyle}
nodeId={id}
isConnectableEnd={false}
></CommonHandle>
<NodeHeader
id={id}
name={'Chunker'}
label={data.label}
icon={<OperatorIcon name={Operator.Splitter}></OperatorIcon>}
></NodeHeader>
<LabelCard>{data.name}</LabelCard>
</NodeWrapper>
</ToolBar>
);
}
export const SplitterNode = memo(InnerSplitterNode);

View File

@ -0,0 +1,55 @@
import { BaseNode } from '@/interfaces/database/agent';
import { NodeProps, Position } from '@xyflow/react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { NodeHandleId } from '../../constant';
import { TokenizerFormSchemaType } from '../../form/tokenizer-form';
import { LabelCard } from './card';
import { CommonHandle } from './handle';
import { LeftHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar';
function TokenizerNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<BaseNode<TokenizerFormSchemaType>>) {
const { t } = useTranslation();
return (
<ToolBar
selected={selected}
id={id}
label={data.label}
showRun={false}
showCopy={false}
>
<NodeWrapper selected={selected}>
<CommonHandle
id={NodeHandleId.End}
type="target"
position={Position.Left}
isConnectable={isConnectable}
style={LeftHandleStyle}
nodeId={id}
></CommonHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<LabelCard className="text-text-primary flex justify-between flex-col gap-1">
<span className="text-text-secondary">
{t('dataflow.searchMethod')}
</span>
<ul className="space-y-1">
{data.form?.search_method.map((x) => (
<li key={x}>{t(`dataflow.tokenizerSearchMethodOptions.${x}`)}</li>
))}
</ul>
</LabelCard>
</NodeWrapper>
</ToolBar>
);
}
export default memo(TokenizerNode);

View File

@ -28,6 +28,7 @@ type ToolBarProps = {
label: string;
id: string;
showRun?: boolean;
showCopy?: boolean;
} & PropsWithChildren;
export function ToolBar({
@ -36,6 +37,7 @@ export function ToolBar({
label,
id,
showRun = true,
showCopy = true,
}: ToolBarProps) {
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
const deleteIterationNodeById = useGraphStore(
@ -74,10 +76,12 @@ export function ToolBar({
<IconWrapper>
<Play className="size-3.5" data-play />
</IconWrapper>
)}{' '}
<IconWrapper onClick={handleDuplicate}>
<Copy className="size-3.5" />
</IconWrapper>
)}
{showCopy && (
<IconWrapper onClick={handleDuplicate}>
<Copy className="size-3.5" />
</IconWrapper>
)}
<IconWrapper onClick={deleteNode}>
<Trash2 className="size-3.5" />
</IconWrapper>

View File

@ -6,9 +6,13 @@ import {
AgentGlobals,
AgentGlobalsSysQueryWithBrace,
CodeTemplateStrMap,
Operator,
ProgrammingLanguage,
initialLlmBaseValues,
} from '@/constants/agent';
export { Operator } from '@/constants/agent';
export * from './pipeline';
export enum AgentDialogueMode {
Conversational = 'conversational',
@ -43,51 +47,6 @@ import {
export const BeginId = 'begin';
export enum Operator {
Begin = 'Begin',
Retrieval = 'Retrieval',
Categorize = 'Categorize',
Message = 'Message',
Relevant = 'Relevant',
RewriteQuestion = 'RewriteQuestion',
KeywordExtract = 'KeywordExtract',
Baidu = 'Baidu',
DuckDuckGo = 'DuckDuckGo',
Wikipedia = 'Wikipedia',
PubMed = 'PubMed',
ArXiv = 'ArXiv',
Google = 'Google',
Bing = 'Bing',
GoogleScholar = 'GoogleScholar',
DeepL = 'DeepL',
GitHub = 'GitHub',
BaiduFanyi = 'BaiduFanyi',
QWeather = 'QWeather',
ExeSQL = 'ExeSQL',
Switch = 'Switch',
WenCai = 'WenCai',
AkShare = 'AkShare',
YahooFinance = 'YahooFinance',
Jin10 = 'Jin10',
TuShare = 'TuShare',
Note = 'Note',
Crawler = 'Crawler',
Invoke = 'Invoke',
Email = 'Email',
Iteration = 'Iteration',
IterationStart = 'IterationItem',
Code = 'CodeExec',
WaitingDialogue = 'WaitingDialogue',
Agent = 'Agent',
Tool = 'Tool',
TavilySearch = 'TavilySearch',
TavilyExtract = 'TavilyExtract',
UserFillUp = 'UserFillUp',
StringTransform = 'StringTransform',
SearXNG = 'SearXNG',
Placeholder = 'Placeholder',
}
export const SwitchLogicOperatorOptions = ['and', 'or'];
export const CommonOperatorList = Object.values(Operator).filter(
@ -833,6 +792,11 @@ export const RestrictedUpstreamMap = {
[Operator.UserFillUp]: [Operator.Begin],
[Operator.Tool]: [Operator.Begin],
[Operator.Placeholder]: [Operator.Begin],
[Operator.Parser]: [Operator.Begin],
[Operator.Splitter]: [Operator.Begin],
[Operator.HierarchicalMerger]: [Operator.Begin],
[Operator.Tokenizer]: [Operator.Begin],
[Operator.Extractor]: [Operator.Begin],
};
export const NodeMap = {
@ -878,6 +842,12 @@ export const NodeMap = {
[Operator.StringTransform]: 'ragNode',
[Operator.TavilyExtract]: 'ragNode',
[Operator.Placeholder]: 'placeholderNode',
[Operator.File]: 'fileNode',
[Operator.Parser]: 'parserNode',
[Operator.Tokenizer]: 'tokenizerNode',
[Operator.Splitter]: 'splitterNode',
[Operator.HierarchicalMerger]: 'splitterNode',
[Operator.Extractor]: 'contextNode',
};
export enum BeginQueryType {
@ -906,6 +876,21 @@ export const NoDebugOperatorsList = [
Operator.Iteration,
Operator.UserFillUp,
Operator.IterationStart,
Operator.File,
Operator.Parser,
Operator.Tokenizer,
Operator.Splitter,
Operator.HierarchicalMerger,
Operator.Extractor,
];
export const NoCopyOperatorsList = [
Operator.File,
Operator.Parser,
Operator.Tokenizer,
Operator.Splitter,
Operator.HierarchicalMerger,
Operator.Extractor,
];
export enum NodeHandleId {

View File

@ -0,0 +1,272 @@
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
import {
initialLlmBaseValues,
DataflowOperator as Operator,
} from '@/constants/agent';
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 const FileId = 'File'; // BeginId
export enum TokenizerSearchMethod {
Embedding = 'embedding',
FullText = 'full_text',
}
export enum ImageParseMethod {
OCR = 'ocr',
}
export enum TokenizerFields {
Text = 'text',
Questions = 'questions',
Summary = 'summary',
}
export enum ParserFields {
From = 'from',
To = 'to',
Cc = 'cc',
Bcc = 'bcc',
Date = 'date',
Subject = 'subject',
Body = 'body',
Attachments = 'attachments',
}
// initialBeginValues
export const initialFileValues = {
outputs: {
name: {
type: 'string',
value: '',
},
file: {
type: 'Object',
value: {},
},
},
};
export const initialTokenizerValues = {
search_method: [
TokenizerSearchMethod.Embedding,
TokenizerSearchMethod.FullText,
],
filename_embd_weight: 0.1,
fields: TokenizerFields.Text,
outputs: {},
};
export enum StringTransformMethod {
Merge = 'merge',
Split = 'split',
}
export enum StringTransformDelimiter {
Comma = ',',
Semicolon = ';',
Period = '.',
LineBreak = '\n',
Tab = '\t',
Space = ' ',
}
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,
system_prompt: '',
},
{
fileFormat: FileType.Email,
fields: Object.values(ParserFields),
output_format: EmailOutputFormat.Text,
},
{
fileFormat: FileType.TextMarkdown,
output_format: TextMarkdownOutputFormat.Text,
},
{
fileFormat: FileType.Docx,
output_format: DocxOutputFormat.Json,
},
{
fileFormat: FileType.PowerPoint,
output_format: PptOutputFormat.Json,
},
],
};
export const initialSplitterValues = {
outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
chunk_token_size: 512,
overlapped_percent: 0,
delimiters: [{ value: '\n' }],
};
export enum Hierarchy {
H1 = '1',
H2 = '2',
H3 = '3',
H4 = '4',
H5 = '5',
}
export const initialHierarchicalMergerValues = {
outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
hierarchy: Hierarchy.H3,
levels: [
{ expressions: [{ expression: '^#[^#]' }] },
{ expressions: [{ expression: '^##[^#]' }] },
{ expressions: [{ expression: '^###[^#]' }] },
{ expressions: [{ expression: '^####[^#]' }] },
],
};
export const initialExtractorValues = {
...initialLlmBaseValues,
field_name: ContextGeneratorFieldName.Summary,
outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
};
export const NoDebugOperatorsList = [Operator.Begin];
export const FileTypeSuffixMap = {
[FileType.PDF]: ['pdf'],
[FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'],
[FileType.Image]: ['jpg', 'jpeg', 'png', 'gif'],
[FileType.Email]: ['eml', 'msg'],
[FileType.TextMarkdown]: ['md', 'markdown', 'mdx', 'txt'],
[FileType.Docx]: ['doc', 'docx'],
[FileType.PowerPoint]: ['pptx'],
[FileType.Video]: [],
[FileType.Audio]: [
'da',
'wave',
'wav',
'mp3',
'aac',
'flac',
'ogg',
'aiff',
'au',
'midi',
'wma',
'realaudio',
'vqf',
'oggvorbis',
'ape',
],
};
export const SingleOperators = [
Operator.Tokenizer,
Operator.Splitter,
Operator.HierarchicalMerger,
Operator.Parser,
];

View File

@ -48,3 +48,13 @@ export type HandleContextType = {
export const HandleContext = createContext<HandleContextType>(
{} as HandleContextType,
);
export type PipelineLogContextType = {
messageId: string;
setMessageId: (messageId: string) => void;
setUploadedFileData: (data: Record<string, any>) => void;
};
export const PipelineLogContext = createContext<PipelineLogContextType>(
{} as PipelineLogContextType,
);

View File

@ -13,25 +13,30 @@ import DeepLForm from '../form/deepl-form';
import DuckDuckGoForm from '../form/duckduckgo-form';
import EmailForm from '../form/email-form';
import ExeSQLForm from '../form/exesql-form';
import ExtractorForm from '../form/extractor-form';
import GithubForm from '../form/github-form';
import GoogleForm from '../form/google-form';
import GoogleScholarForm from '../form/google-scholar-form';
import HierarchicalMergerForm from '../form/hierarchical-merger-form';
import InvokeForm from '../form/invoke-form';
import IterationForm from '../form/iteration-form';
import IterationStartForm from '../form/iteration-start-from';
import Jin10Form from '../form/jin10-form';
import KeywordExtractForm from '../form/keyword-extract-form';
import MessageForm from '../form/message-form';
import ParserForm from '../form/parser-form';
import PubMedForm from '../form/pubmed-form';
import QWeatherForm from '../form/qweather-form';
import RelevantForm from '../form/relevant-form';
import RetrievalForm from '../form/retrieval-form/next';
import RewriteQuestionForm from '../form/rewrite-question-form';
import SearXNGForm from '../form/searxng-form';
import SplitterForm from '../form/splitter-form';
import StringTransformForm from '../form/string-transform-form';
import SwitchForm from '../form/switch-form';
import TavilyExtractForm from '../form/tavily-extract-form';
import TavilyForm from '../form/tavily-form';
import TokenizerForm from '../form/tokenizer-form';
import ToolForm from '../form/tool-form';
import TuShareForm from '../form/tushare-form';
import UserFillUpForm from '../form/user-fill-up-form';
@ -163,4 +168,26 @@ export const FormConfigMap = {
[Operator.TavilyExtract]: {
component: TavilyExtractForm,
},
[Operator.Placeholder]: {
component: () => <></>,
},
// pipeline
[Operator.File]: {
component: () => <></>,
},
[Operator.Parser]: {
component: ParserForm,
},
[Operator.Tokenizer]: {
component: TokenizerForm,
},
[Operator.Splitter]: {
component: SplitterForm,
},
[Operator.HierarchicalMerger]: {
component: HierarchicalMergerForm,
},
[Operator.Extractor]: {
component: ExtractorForm,
},
};

View File

@ -0,0 +1,107 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { LargeModelFormField } from '@/components/large-model-form-field';
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Form } from '@/components/ui/form';
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,
initialExtractorValues,
} from '../../constant/pipeline';
import { useBuildNodeOutputOptions } from '../../hooks/use-build-options';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { useSwitchPrompt } from './use-switch-prompt';
export const FormSchema = z.object({
field_name: z.string(),
sys_prompt: z.string(),
prompts: z.string().optional(),
...LlmSettingSchema,
});
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
const outputList = buildOutputList(initialExtractorValues.outputs);
const ExtractorForm = ({ node }: INextOperatorForm) => {
const defaultValues = useFormValues(initialExtractorValues, node);
const { t } = useTranslation();
const form = useForm<ExtractorFormSchemaType>({
defaultValues,
resolver: zodResolver(FormSchema),
// mode: 'onChange',
});
const promptOptions = useBuildNodeOutputOptions(node?.id);
const options = buildOptions(ContextGeneratorFieldName, t, 'dataflow');
const {
handleFieldNameChange,
confirmSwitch,
hideModal,
visible,
cancelSwitch,
} = useSwitchPrompt(form);
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<LargeModelFormField></LargeModelFormField>
<RAGFlowFormItem label={t('dataflow.fieldName')} name="field_name">
{(field) => (
<SelectWithSearch
onChange={(value) => {
field.onChange(value);
handleFieldNameChange(value);
}}
value={field.value}
placeholder={t('dataFlowPlaceholder')}
options={options}
></SelectWithSearch>
)}
</RAGFlowFormItem>
<RAGFlowFormItem label={t('flow.systemPrompt')} name="sys_prompt">
<PromptEditor
placeholder={t('flow.messagePlaceholder')}
showToolbar={true}
baseOptions={promptOptions}
></PromptEditor>
</RAGFlowFormItem>
<RAGFlowFormItem label={t('flow.userPrompt')} name="prompts">
<PromptEditor
showToolbar={true}
baseOptions={promptOptions}
></PromptEditor>
</RAGFlowFormItem>
<Output list={outputList}></Output>
</FormWrapper>
{visible && (
<ConfirmDeleteDialog
title={t('dataflow.switchPromptMessage')}
open
onOpenChange={hideModal}
onOk={confirmSwitch}
onCancel={cancelSwitch}
></ConfirmDeleteDialog>
)}
</Form>
);
};
export default memo(ExtractorForm);

View File

@ -0,0 +1,69 @@
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { useSetModalState } from '@/hooks/common-hooks';
import { useCallback, useRef } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
export const FormSchema = z.object({
field_name: z.string(),
sys_prompt: z.string(),
prompts: z.string().optional(),
...LlmSettingSchema,
});
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
export function useSwitchPrompt(form: UseFormReturn<ExtractorFormSchemaType>) {
const { visible, showModal, hideModal } = useSetModalState();
const { t } = useTranslation();
const previousFieldNames = useRef<string[]>([form.getValues('field_name')]);
const setPromptValue = useCallback(
(field: keyof ExtractorFormSchemaType, key: string, value: string) => {
form.setValue(field, t(`dataflow.prompts.${key}.${value}`), {
shouldDirty: true,
shouldValidate: true,
});
},
[form, t],
);
const handleFieldNameChange = useCallback(
(value: string) => {
if (value) {
const names = previousFieldNames.current;
if (names.length > 1) {
names.shift();
}
names.push(value);
showModal();
}
},
[showModal],
);
const confirmSwitch = useCallback(() => {
const value = form.getValues('field_name');
setPromptValue('sys_prompt', 'system', value);
setPromptValue('prompts', 'user', value);
}, [form, setPromptValue]);
const cancelSwitch = useCallback(() => {
const previousValue = previousFieldNames.current.at(-2);
if (previousValue) {
form.setValue('field_name', previousValue, {
shouldDirty: true,
shouldValidate: true,
});
}
}, [form]);
return {
handleFieldNameChange,
confirmSwitch,
hideModal,
visible,
cancelSwitch,
};
}

View File

@ -0,0 +1,191 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { BlockButton, Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Form, FormLabel } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { Plus, Trash2 } from 'lucide-react';
import { memo } from 'react';
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
Hierarchy,
initialHierarchicalMergerValues,
} from '../../constant/pipeline';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
const outputList = buildOutputList(initialHierarchicalMergerValues.outputs);
const HierarchyOptions = [
{ label: 'H1', value: Hierarchy.H1 },
{ label: 'H2', value: Hierarchy.H2 },
{ label: 'H3', value: Hierarchy.H3 },
{ label: 'H4', value: Hierarchy.H4 },
{ label: 'H5', value: Hierarchy.H5 },
];
export const FormSchema = z.object({
hierarchy: z.string(),
levels: z.array(
z.object({
expressions: z.array(
z.object({
expression: z.string().refine(
(val) => {
try {
// Try converting the string to a RegExp
new RegExp(val);
return true;
} catch {
return false;
}
},
{
message: 'Must be a valid regular expression string',
},
),
}),
),
}),
),
});
export type HierarchicalMergerFormSchemaType = z.infer<typeof FormSchema>;
type RegularExpressionsProps = {
index: number;
parentName: string;
removeParent: (index: number) => void;
isLatest: boolean;
};
export function RegularExpressions({
index,
parentName,
isLatest,
removeParent,
}: RegularExpressionsProps) {
const { t } = useTranslation();
const form = useFormContext();
const name = `${parentName}.${index}.expressions`;
const { fields, append, remove } = useFieldArray({
name: name,
control: form.control,
});
return (
<Card>
<CardHeader className="flex-row justify-between items-center">
<span>H{index + 1}</span>
{isLatest && (
<Button
type="button"
variant={'ghost'}
onClick={() => removeParent(index)}
>
<Trash2 />
</Button>
)}
</CardHeader>
<CardContent>
<FormLabel required className="mb-2 text-text-secondary">
{t('dataflow.regularExpressions')}
</FormLabel>
<section className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex items-center gap-2">
<div className="space-y-2 flex-1">
<RAGFlowFormItem
name={`${name}.${index}.expression`}
label={'expression'}
labelClassName="!hidden"
>
<Input className="!m-0"></Input>
</RAGFlowFormItem>
</div>
{index === 0 ? (
<Button
onClick={() => append({ expression: '' })}
variant={'ghost'}
>
<Plus></Plus>
</Button>
) : (
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<Trash2 />
</Button>
)}
</div>
))}
</section>
</CardContent>
</Card>
);
}
const HierarchicalMergerForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslation();
const defaultValues = useFormValues(initialHierarchicalMergerValues, node);
const form = useForm<HierarchicalMergerFormSchemaType>({
defaultValues,
resolver: zodResolver(FormSchema),
mode: 'onChange',
});
const name = 'levels';
const { fields, append, remove } = useFieldArray({
name: name,
control: form.control,
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<RAGFlowFormItem name={'hierarchy'} label={t('dataflow.hierarchy')}>
<SelectWithSearch options={HierarchyOptions}></SelectWithSearch>
</RAGFlowFormItem>
{fields.map((field, index) => (
<div key={field.id} className="flex items-center">
<div className="flex-1">
<RegularExpressions
parentName={name}
index={index}
removeParent={remove}
isLatest={index === fields.length - 1}
></RegularExpressions>
</div>
</div>
))}
{fields.length < 5 && (
<BlockButton
onClick={() => append({ expressions: [{ expression: '' }] })}
>
{t('common.add')}
</BlockButton>
)}
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
};
export default memo(HierarchicalMergerForm);

View File

@ -0,0 +1,106 @@
import { crossLanguageOptions } from '@/components/cross-language-form-field';
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
import {
LLMFormField,
LLMFormFieldProps,
} from '@/components/llm-setting-items/llm-form-field';
import {
SelectWithSearch,
SelectWithSearchFlagOptionType,
} from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { upperCase, upperFirst } from 'lodash';
import { useTranslation } from 'react-i18next';
import {
FileType,
OutputFormatMap,
SpreadsheetOutputFormat,
} from '../../constant/pipeline';
import { CommonProps } from './interface';
import { buildFieldNameWithPrefix } from './utils';
const UppercaseFields = [
SpreadsheetOutputFormat.Html,
SpreadsheetOutputFormat.Json,
];
function buildOutputOptionsFormatMap() {
return Object.entries(OutputFormatMap).reduce<
Record<string, SelectWithSearchFlagOptionType[]>
>((pre, [key, value]) => {
pre[key] = Object.values(value).map((v) => ({
label: UppercaseFields.some((x) => x === v)
? upperCase(v)
: upperFirst(v),
value: v,
}));
return pre;
}, {});
}
export type OutputFormatFormFieldProps = CommonProps & {
fileType: FileType;
};
export function OutputFormatFormField({
prefix,
fileType,
}: OutputFormatFormFieldProps) {
const { t } = useTranslation();
return (
<RAGFlowFormItem
name={buildFieldNameWithPrefix(`output_format`, prefix)}
label={t('dataflow.outputFormat')}
>
<SelectWithSearch
options={buildOutputOptionsFormatMap()[fileType]}
></SelectWithSearch>
</RAGFlowFormItem>
);
}
export function ParserMethodFormField({
prefix,
optionsWithoutLLM,
}: CommonProps & { optionsWithoutLLM?: { value: string; label: string }[] }) {
const { t } = useTranslation();
return (
<LayoutRecognizeFormField
name={buildFieldNameWithPrefix(`parse_method`, prefix)}
horizontal={false}
optionsWithoutLLM={optionsWithoutLLM}
label={t('dataflow.parserMethod')}
></LayoutRecognizeFormField>
);
}
export function LargeModelFormField({
prefix,
options,
}: CommonProps & Pick<LLMFormFieldProps, 'options'>) {
return (
<LLMFormField
name={buildFieldNameWithPrefix('llm_id', prefix)}
options={options}
></LLMFormField>
);
}
export function LanguageFormField({ prefix }: CommonProps) {
const { t } = useTranslation();
return (
<RAGFlowFormItem
name={buildFieldNameWithPrefix(`lang`, prefix)}
label={t('dataflow.lang')}
>
{(field) => (
<SelectWithSearch
options={crossLanguageOptions}
value={field.value}
onChange={field.onChange}
></SelectWithSearch>
)}
</RAGFlowFormItem>
);
}

View File

@ -0,0 +1,30 @@
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { MultiSelect } from '@/components/ui/multi-select';
import { buildOptions } from '@/utils/form';
import { useTranslation } from 'react-i18next';
import { ParserFields } from '../../constant/pipeline';
import { CommonProps } from './interface';
import { buildFieldNameWithPrefix } from './utils';
const options = buildOptions(ParserFields);
export function EmailFormFields({ prefix }: CommonProps) {
const { t } = useTranslation();
return (
<>
<RAGFlowFormItem
name={buildFieldNameWithPrefix(`fields`, prefix)}
label={t('dataflow.fields')}
>
{(field) => (
<MultiSelect
options={options}
onValueChange={field.onChange}
defaultValue={field.value}
variant="inverted"
></MultiSelect>
)}
</RAGFlowFormItem>
</>
);
}

View File

@ -0,0 +1,60 @@
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Textarea } from '@/components/ui/textarea';
import { buildOptions } from '@/utils/form';
import { isEmpty } from 'lodash';
import { useEffect, useMemo } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { ImageParseMethod } from '../../constant/pipeline';
import { LanguageFormField, ParserMethodFormField } from './common-form-fields';
import { CommonProps } from './interface';
import { useSetInitialLanguage } from './use-set-initial-language';
import { buildFieldNameWithPrefix } from './utils';
export function ImageFormFields({ prefix }: CommonProps) {
const { t } = useTranslation();
const form = useFormContext();
const options = buildOptions(
ImageParseMethod,
t,
'dataflow.imageParseMethodOptions',
);
const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix);
const parseMethod = useWatch({
name: parseMethodName,
});
const languageShown = useMemo(() => {
return !isEmpty(parseMethod) && parseMethod !== ImageParseMethod.OCR;
}, [parseMethod]);
useEffect(() => {
if (isEmpty(form.getValues(parseMethodName))) {
form.setValue(parseMethodName, ImageParseMethod.OCR, {
shouldValidate: true,
shouldDirty: true,
});
}
}, [form, parseMethodName]);
useSetInitialLanguage({ prefix, languageShown });
return (
<>
<ParserMethodFormField
prefix={prefix}
optionsWithoutLLM={options}
></ParserMethodFormField>
{languageShown && <LanguageFormField prefix={prefix}></LanguageFormField>}
{languageShown && (
<RAGFlowFormItem
name={buildFieldNameWithPrefix('system_prompt', prefix)}
label={t('dataflow.systemPrompt')}
>
<Textarea placeholder={t('dataflow.systemPromptPlaceholder')} />
</RAGFlowFormItem>
)}
</>
);
}

View File

@ -0,0 +1,226 @@
import {
SelectWithSearch,
SelectWithSearchFlagOptionType,
} from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { BlockButton, Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useHover } from 'ahooks';
import { Trash2 } from 'lucide-react';
import { memo, useCallback, useMemo, useRef } from 'react';
import {
UseFieldArrayRemove,
useFieldArray,
useForm,
useFormContext,
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
FileType,
InitialOutputFormatMap,
initialParserValues,
} from '../../constant/pipeline';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { Output } from '../components/output';
import { OutputFormatFormField } from './common-form-fields';
import { EmailFormFields } from './email-form-fields';
import { ImageFormFields } from './image-form-fields';
import { PdfFormFields } from './pdf-form-fields';
import { buildFieldNameWithPrefix } from './utils';
import { VideoFormFields } from './video-form-fields';
const outputList = buildOutputList(initialParserValues.outputs);
const FileFormatWidgetMap = {
[FileType.PDF]: PdfFormFields,
[FileType.Video]: VideoFormFields,
[FileType.Audio]: VideoFormFields,
[FileType.Email]: EmailFormFields,
[FileType.Image]: ImageFormFields,
};
type ParserItemProps = {
name: string;
index: number;
fieldLength: number;
remove: UseFieldArrayRemove;
fileFormatOptions: SelectWithSearchFlagOptionType[];
};
export const FormSchema = z.object({
setups: z.array(
z.object({
fileFormat: z.string().nullish(),
output_format: z.string().optional(),
parse_method: z.string().optional(),
lang: z.string().optional(),
fields: z.array(z.string()).optional(),
llm_id: z.string().optional(),
system_prompt: z.string().optional(),
}),
),
});
export type ParserFormSchemaType = z.infer<typeof FormSchema>;
function ParserItem({
name,
index,
fieldLength,
remove,
fileFormatOptions,
}: ParserItemProps) {
const { t } = useTranslation();
const form = useFormContext<ParserFormSchemaType>();
const ref = useRef(null);
const isHovering = useHover(ref);
const prefix = `${name}.${index}`;
const fileFormat = form.getValues(`setups.${index}.fileFormat`);
const values = form.getValues();
const parserList = values.setups.slice(); // Adding, deleting, or modifying the parser array will not change the reference.
const filteredFileFormatOptions = useMemo(() => {
const otherFileFormatList = parserList
.filter((_, idx) => idx !== index)
.map((x) => x.fileFormat);
return fileFormatOptions.filter((x) => {
return !otherFileFormatList.includes(x.value);
});
}, [fileFormatOptions, index, parserList]);
const Widget =
typeof fileFormat === 'string' && fileFormat in FileFormatWidgetMap
? FileFormatWidgetMap[fileFormat as keyof typeof FileFormatWidgetMap]
: () => <></>;
const handleFileTypeChange = useCallback(
(value: FileType) => {
form.setValue(
`setups.${index}.output_format`,
InitialOutputFormatMap[value],
{ shouldDirty: true, shouldValidate: true, shouldTouch: true },
);
},
[form, index],
);
return (
<section
className={cn('space-y-5 py-2.5 rounded-md', {
'bg-state-error-5': isHovering,
})}
>
<div className="flex justify-between items-center">
<span className="text-text-primary text-sm font-medium">
Parser {index + 1}
</span>
{index > 0 && (
<Button variant={'ghost'} onClick={() => remove(index)} ref={ref}>
<Trash2 />
</Button>
)}
</div>
<RAGFlowFormItem
name={buildFieldNameWithPrefix(`fileFormat`, prefix)}
label={t('dataflow.fileFormats')}
>
{(field) => (
<SelectWithSearch
value={field.value}
onChange={(val) => {
field.onChange(val);
handleFileTypeChange(val as FileType);
}}
options={filteredFileFormatOptions}
></SelectWithSearch>
)}
</RAGFlowFormItem>
<Widget prefix={prefix} fileType={fileFormat as FileType}></Widget>
<div className="hidden">
<OutputFormatFormField
prefix={prefix}
fileType={fileFormat as FileType}
/>
</div>
{index < fieldLength - 1 && <Separator />}
</section>
);
}
const ParserForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslation();
const defaultValues = useFormValues(initialParserValues, node);
const FileFormatOptions = buildOptions(
FileType,
t,
'dataflow.fileFormatOptions',
).filter(
(x) => x.value !== FileType.Video, // Temporarily hide the video option
);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),
shouldUnregister: true,
});
const name = 'setups';
const { fields, remove, append } = useFieldArray({
name,
control: form.control,
});
const add = useCallback(() => {
append({
fileFormat: null,
output_format: '',
parse_method: '',
lang: '',
fields: [],
llm_id: '',
});
}, [append]);
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<form className="px-5">
{fields.map((field, index) => {
return (
<ParserItem
key={field.id}
name={name}
index={index}
fieldLength={fields.length}
remove={remove}
fileFormatOptions={FileFormatOptions}
></ParserItem>
);
})}
{fields.length < FileFormatOptions.length && (
<BlockButton onClick={add} type="button" className="mt-2.5">
{t('dataflow.addParser')}
</BlockButton>
)}
</form>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
};
export default memo(ParserForm);

View File

@ -0,0 +1,3 @@
export type CommonProps = {
prefix: string;
};

View File

@ -0,0 +1,44 @@
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
import { isEmpty } from 'lodash';
import { useEffect, useMemo } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { LanguageFormField, ParserMethodFormField } from './common-form-fields';
import { CommonProps } from './interface';
import { useSetInitialLanguage } from './use-set-initial-language';
import { buildFieldNameWithPrefix } from './utils';
export function PdfFormFields({ prefix }: CommonProps) {
const form = useFormContext();
const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix);
const parseMethod = useWatch({
name: parseMethodName,
});
const languageShown = useMemo(() => {
return (
!isEmpty(parseMethod) &&
parseMethod !== ParseDocumentType.DeepDOC &&
parseMethod !== ParseDocumentType.PlainText
);
}, [parseMethod]);
useSetInitialLanguage({ prefix, languageShown });
useEffect(() => {
if (isEmpty(form.getValues(parseMethodName))) {
form.setValue(parseMethodName, ParseDocumentType.DeepDOC, {
shouldValidate: true,
shouldDirty: true,
});
}
}, [form, parseMethodName]);
return (
<>
<ParserMethodFormField prefix={prefix}></ParserMethodFormField>
{languageShown && <LanguageFormField prefix={prefix}></LanguageFormField>}
</>
);
}

View File

@ -0,0 +1,29 @@
import { crossLanguageOptions } from '@/components/cross-language-form-field';
import { isEmpty } from 'lodash';
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
import { buildFieldNameWithPrefix } from './utils';
export function useSetInitialLanguage({
prefix,
languageShown,
}: {
prefix: string;
languageShown: boolean;
}) {
const form = useFormContext();
const lang = form.getValues(buildFieldNameWithPrefix('lang', prefix));
useEffect(() => {
if (languageShown && isEmpty(lang)) {
form.setValue(
buildFieldNameWithPrefix('lang', prefix),
crossLanguageOptions[0].value,
{
shouldValidate: true,
shouldDirty: true,
},
);
}
}, [form, lang, languageShown, prefix]);
}

View File

@ -0,0 +1,3 @@
export function buildFieldNameWithPrefix(name: string, prefix: string) {
return `${prefix}.${name}`;
}

View File

@ -0,0 +1,22 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import {
LargeModelFormField,
OutputFormatFormFieldProps,
} from './common-form-fields';
export function VideoFormFields({ prefix }: OutputFormatFormFieldProps) {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Speech2text,
]);
return (
<>
{/* Multimodal Model */}
<LargeModelFormField
prefix={prefix}
options={modelOptions}
></LargeModelFormField>
</>
);
}

View File

@ -0,0 +1,101 @@
import { DelimiterInput } from '@/components/delimiter-form-field';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { SliderInputFormField } from '@/components/slider-input-form-field';
import { BlockButton, Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trash2 } from 'lucide-react';
import { memo } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { initialSplitterValues } from '../../constant/pipeline';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
const outputList = buildOutputList(initialSplitterValues.outputs);
export const FormSchema = z.object({
chunk_token_size: z.number(),
delimiters: z.array(
z.object({
value: z.string().optional(),
}),
),
overlapped_percent: z.number(), // 0.0 - 0.3 , 0% - 30%
});
export type SplitterFormSchemaType = z.infer<typeof FormSchema>;
const SplitterForm = ({ node }: INextOperatorForm) => {
const defaultValues = useFormValues(initialSplitterValues, node);
const { t } = useTranslation();
const form = useForm<SplitterFormSchemaType>({
defaultValues,
resolver: zodResolver(FormSchema),
});
const name = 'delimiters';
const { fields, append, remove } = useFieldArray({
name: name,
control: form.control,
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<SliderInputFormField
name="chunk_token_size"
max={2048}
label={t('knowledgeConfiguration.chunkTokenNumber')}
></SliderInputFormField>
<SliderInputFormField
name="overlapped_percent"
max={30}
min={0}
label={t('dataflow.overlappedPercent')}
></SliderInputFormField>
<section>
<span className="mb-2 inline-block">{t('flow.delimiters')}</span>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex items-center gap-2">
<div className="space-y-2 flex-1">
<RAGFlowFormItem
name={`${name}.${index}.value`}
label="delimiter"
labelClassName="!hidden"
>
<DelimiterInput className="!m-0"></DelimiterInput>
</RAGFlowFormItem>
</div>
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<Trash2 />
</Button>
</div>
))}
</div>
</section>
<BlockButton onClick={() => append({ value: '\n' })}>
{t('common.add')}
</BlockButton>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
};
export default memo(SplitterForm);

View File

@ -0,0 +1,91 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { SliderInputFormField } from '@/components/slider-input-form-field';
import { Form } from '@/components/ui/form';
import { MultiSelect } from '@/components/ui/multi-select';
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 {
initialTokenizerValues,
TokenizerFields,
TokenizerSearchMethod,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
const outputList = buildOutputList(initialTokenizerValues.outputs);
export const FormSchema = z.object({
search_method: z.array(z.string()).min(1),
filename_embd_weight: z.number(),
fields: z.string(),
});
export type TokenizerFormSchemaType = z.infer<typeof FormSchema>;
const TokenizerForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslation();
const defaultValues = useFormValues(initialTokenizerValues, node);
const SearchMethodOptions = buildOptions(
TokenizerSearchMethod,
t,
`dataflow.tokenizerSearchMethodOptions`,
);
const FieldsOptions = buildOptions(
TokenizerFields,
t,
'dataflow.tokenizerFieldsOptions',
);
const form = useForm<TokenizerFormSchemaType>({
defaultValues,
resolver: zodResolver(FormSchema),
mode: 'onChange',
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<RAGFlowFormItem
name="search_method"
label={t('dataflow.searchMethod')}
tooltip={t('dataflow.searchMethodTip')}
>
{(field) => (
<MultiSelect
options={SearchMethodOptions}
onValueChange={field.onChange}
defaultValue={field.value}
variant="inverted"
/>
)}
</RAGFlowFormItem>
<SliderInputFormField
name="filename_embd_weight"
label={t('dataflow.filenameEmbeddingWeight')}
max={0.5}
step={0.01}
></SliderInputFormField>
<RAGFlowFormItem name="fields" label={t('dataflow.fields')}>
{(field) => <SelectWithSearch options={FieldsOptions} {...field} />}
</RAGFlowFormItem>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
};
export default memo(TokenizerForm);

View File

@ -23,9 +23,11 @@ import {
initialDuckValues,
initialEmailValues,
initialExeSqlValues,
initialExtractorValues,
initialGithubValues,
initialGoogleScholarValues,
initialGoogleValues,
initialHierarchicalMergerValues,
initialInvokeValues,
initialIterationStartValues,
initialIterationValues,
@ -33,16 +35,19 @@ import {
initialKeywordExtractValues,
initialMessageValues,
initialNoteValues,
initialParserValues,
initialPubMedValues,
initialQWeatherValues,
initialRelevantValues,
initialRetrievalValues,
initialRewriteQuestionValues,
initialSearXNGValues,
initialSplitterValues,
initialStringTransformValues,
initialSwitchValues,
initialTavilyExtractValues,
initialTavilyValues,
initialTokenizerValues,
initialTuShareValues,
initialUserFillUpValues,
initialWaitingDialogueValues,
@ -115,6 +120,17 @@ export const useInitializeOperatorParams = () => {
[Operator.StringTransform]: initialStringTransformValues,
[Operator.TavilyExtract]: initialTavilyExtractValues,
[Operator.Placeholder]: {},
[Operator.File]: {},
[Operator.Parser]: initialParserValues,
[Operator.Tokenizer]: initialTokenizerValues,
[Operator.Splitter]: initialSplitterValues,
[Operator.HierarchicalMerger]: initialHierarchicalMergerValues,
[Operator.Extractor]: {
...initialExtractorValues,
llm_id: llmId,
sys_prompt: t('dataflow.prompts.system.summary'),
prompts: t('dataflow.prompts.user.summary'),
},
};
}, [llmId]);

View File

@ -0,0 +1,19 @@
import { buildNodeOutputOptions } from '@/utils/canvas-util';
import { useMemo } from 'react';
import { Operator } from '../constant';
import OperatorIcon from '../operator-icon';
import useGraphStore from '../store';
export function useBuildNodeOutputOptions(nodeId?: string) {
const nodes = useGraphStore((state) => state.nodes);
const edges = useGraphStore((state) => state.edges);
return useMemo(() => {
return buildNodeOutputOptions({
nodes,
edges,
nodeId,
Icon: ({ name }) => <OperatorIcon name={name as Operator}></OperatorIcon>,
});
}, [edges, nodeId, nodes]);
}

View File

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

View File

@ -201,6 +201,7 @@ export const useConnectionDrag = (
}, [removePlaceholderNode, hideModal, clearActiveDropdown]);
return {
nodeId: connectionStartRef.current?.nodeId,
onConnectStart,
onConnectEnd,
handleConnect,

View File

@ -0,0 +1,38 @@
import { useFetchAgent } from '@/hooks/use-agent-request';
import { ITraceData } from '@/interfaces/database/agent';
import { downloadJsonFile } from '@/utils/file-util';
import { get, isEmpty } from 'lodash';
import { useCallback } from 'react';
export function findEndOutput(list?: ITraceData[]) {
if (Array.isArray(list)) {
const trace = list.find((x) => x.component_id === 'END')?.trace;
const str = get(trace, '0.message');
try {
if (!isEmpty(str)) {
const json = JSON.parse(str);
return json;
}
} catch (error) {}
}
}
export function isEndOutputEmpty(list?: ITraceData[]) {
return isEmpty(findEndOutput(list));
}
export function useDownloadOutput(data?: ITraceData[]) {
const { data: agent } = useFetchAgent();
const handleDownloadJson = useCallback(() => {
const output = findEndOutput(data);
if (!isEndOutputEmpty(data)) {
downloadJsonFile(output, `${agent.title}.json`);
}
}, [agent.title, data]);
return {
handleDownloadJson,
};
}

View File

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

View File

@ -0,0 +1,10 @@
import { AgentCategory, AgentQuery } from '@/constants/agent';
import { useSearchParams } from 'umi';
export function useIsPipeline() {
const [queryParameters] = useSearchParams();
return (
queryParameters.get(AgentQuery.Category) === AgentCategory.DataflowCanvas
);
}

View File

@ -17,6 +17,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import message from '@/components/ui/message';
import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
@ -31,20 +32,27 @@ import {
Settings,
Upload,
} from 'lucide-react';
import { ComponentPropsWithoutRef, useCallback } from 'react';
import { ComponentPropsWithoutRef, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import AgentCanvas from './canvas';
import { DropdownProvider } from './canvas/context';
import { Operator } from './constant';
import { PipelineLogContext } from './context';
import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow';
import { useHandleExportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useFetchPipelineLog } from './hooks/use-fetch-pipeline-log';
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
import { useIsPipeline } from './hooks/use-is-pipeline';
import {
useSaveGraph,
useSaveGraphBeforeOpeningDebugDrawer,
useWatchAgentChange,
} from './hooks/use-save-graph';
import { PipelineLogSheet } from './pipeline-log-sheet';
import { SettingDialog } from './setting-dialog';
import useGraphStore from './store';
import { useAgentHistoryManager } from './use-agent-history-manager';
import { VersionDialog } from './version-dialog';
@ -61,6 +69,7 @@ function AgentDropdownMenuItem({
export default function Agent() {
const { id } = useParams();
const isPipeline = useIsPipeline();
const { navigateToAgents } = useNavigatePage();
const {
visible: chatDrawerVisible,
@ -99,6 +108,63 @@ export default function Agent() {
const { navigateToAgentLogs } = useNavigatePage();
const time = useWatchAgentChange(chatDrawerVisible);
// pipeline
const {
visible: pipelineLogSheetVisible,
showModal: showPipelineLogSheet,
hideModal: hidePipelineLogSheet,
} = useSetModalState();
const {
isParsing,
logs,
messageId,
setMessageId,
isCompleted,
stopFetchTrace,
isLogEmpty,
} = useFetchPipelineLog(pipelineLogSheetVisible);
const [uploadedFileData, setUploadedFileData] =
useState<Record<string, any>>();
const findNodeByName = useGraphStore((state) => state.findNodeByName);
const handleRunPipeline = useCallback(() => {
if (!findNodeByName(Operator.Tokenizer)) {
message.warning(t('dataflow.tokenizerRequired'));
return;
}
if (isParsing) {
// show log sheet
showPipelineLogSheet();
} else {
hidePipelineLogSheet();
handleRun();
}
}, [
findNodeByName,
handleRun,
hidePipelineLogSheet,
isParsing,
showPipelineLogSheet,
t,
]);
const { handleCancel } = useCancelCurrentDataflow({
messageId,
stopFetchTrace,
});
const run = useCallback(() => {
if (isPipeline) {
handleRunPipeline();
} else {
handleRunAgent();
}
}, [handleRunAgent, handleRunPipeline, isPipeline]);
return (
<section className="h-full">
<PageHeader>
@ -128,7 +194,7 @@ export default function Agent() {
>
<LaptopMinimalCheck /> {t('flow.save')}
</ButtonLoading>
<Button variant={'secondary'} onClick={handleRunAgent}>
<Button variant={'secondary'} onClick={run}>
<CirclePlay />
{t('flow.run')}
</Button>
@ -136,14 +202,15 @@ export default function Agent() {
<History />
{t('flow.historyversion')}
</Button>
<Button
variant={'secondary'}
onClick={navigateToAgentLogs(id as string)}
>
<Logs />
{t('flow.log')}
</Button>
{isPipeline || (
<Button
variant={'secondary'}
onClick={navigateToAgentLogs(id as string)}
>
<Logs />
{t('flow.log')}
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'secondary'}>
@ -160,27 +227,32 @@ export default function Agent() {
<Settings />
{t('flow.setting')}
</AgentDropdownMenuItem>
{location.hostname !== 'demo.ragflow.io' && (
<>
<DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={showEmbedModal}>
<ScreenShare />
{t('common.embedIntoSite')}
</AgentDropdownMenuItem>
</>
)}
{isPipeline ||
(location.hostname !== 'demo.ragflow.io' && (
<>
<DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={showEmbedModal}>
<ScreenShare />
{t('common.embedIntoSite')}
</AgentDropdownMenuItem>
</>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</PageHeader>
<ReactFlowProvider>
<DropdownProvider>
<AgentCanvas
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
></AgentCanvas>
</DropdownProvider>
</ReactFlowProvider>
<PipelineLogContext.Provider
value={{ messageId, setMessageId, setUploadedFileData }}
>
<ReactFlowProvider>
<DropdownProvider>
<AgentCanvas
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
></AgentCanvas>
</DropdownProvider>
</ReactFlowProvider>
</PipelineLogContext.Provider>
{embedVisible && (
<EmbedDialog
visible={embedVisible}
@ -199,6 +271,19 @@ export default function Agent() {
{settingDialogVisible && (
<SettingDialog hideModal={hideSettingDialog}></SettingDialog>
)}
{pipelineLogSheetVisible && (
<PipelineLogSheet
hideModal={hidePipelineLogSheet}
isParsing={isParsing}
isCompleted={isCompleted}
isLogEmpty={isLogEmpty}
logs={logs}
handleCancel={handleCancel}
messageId={messageId}
uploadedFileData={uploadedFileData}
></PipelineLogSheet>
)}
</section>
);
}

View File

@ -0,0 +1,137 @@
import {
Timeline,
TimelineContent,
TimelineHeader,
TimelineIndicator,
TimelineItem,
TimelineSeparator,
TimelineTitle,
} from '@/components/originui/timeline';
import { Progress } from '@/components/ui/progress';
import { ITraceData } from '@/interfaces/database/agent';
import { cn } from '@/lib/utils';
import { isEmpty } from 'lodash';
import { File } from 'lucide-react';
import { useCallback } from 'react';
import { Operator } from '../constant';
import OperatorIcon from '../operator-icon';
import useGraphStore from '../store';
export type DataflowTimelineProps = {
traceList?: ITraceData[];
};
const END = 'END';
interface DataflowTrace {
datetime: string;
elapsed_time: number;
message: string;
progress: number;
timestamp: number;
}
export function DataflowTimeline({ traceList }: DataflowTimelineProps) {
const getNode = useGraphStore((state) => state.getNode);
const getNodeData = useCallback(
(componentId: string) => {
return getNode(componentId)?.data;
},
[getNode],
);
const getNodeLabel = useCallback(
(componentId: string) => {
return getNodeData(componentId)?.label as Operator;
},
[getNodeData],
);
return (
<Timeline>
{Array.isArray(traceList) &&
traceList?.map((item, index) => {
const traces = item.trace as DataflowTrace[];
const nodeLabel = getNodeLabel(item.component_id);
const latest = traces[traces.length - 1];
const progress = latest.progress * 100;
return (
<TimelineItem
key={item.component_id}
step={index}
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8 pb-6"
>
<TimelineHeader>
<TimelineSeparator className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-7 bg-accent-primary" />
<TimelineTitle className="">
<TimelineContent
className={cn(
'text-foreground rounded-lg border px-4 py-3',
)}
>
<section className="flex items-center justify-between mb-2">
<span className="flex-1 truncate">
{getNodeData(item.component_id)?.name || END}
</span>
<div className="flex-1 flex items-center gap-5">
<Progress value={progress} className="h-1 flex-1" />
<span className="text-accent-primary text-xs">
{progress.toFixed(2)}%
</span>
</div>
</section>
<div className="divide-y space-y-1">
{traces
.filter((x) => !isEmpty(x.message))
.map((x, idx) => (
<section
key={idx}
className="text-text-secondary text-xs space-x-2 py-2.5 !m-0"
>
<span>{x.datetime}</span>
{item.component_id !== 'END' && (
<span
className={cn({
'text-state-error':
x.message.startsWith('[ERROR]'),
})}
>
{x.message}
</span>
)}
<span>
{x.elapsed_time.toString().slice(0, 6)}s
</span>
</section>
))}
</div>
</TimelineContent>
</TimelineTitle>
<TimelineIndicator
className={cn(
'border border-accent-primary group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-5 items-center justify-center group-data-[orientation=vertical]/timeline:-left-7',
{
'rounded bg-accent-primary': nodeLabel === Operator.Begin,
},
)}
>
{item.component_id === END ? (
<span className="rounded-full inline-block size-2 bg-accent-primary"></span>
) : nodeLabel === Operator.Begin ? (
<File className="size-3.5 text-bg-base"></File>
) : (
<OperatorIcon
name={nodeLabel}
className="size-3.5 rounded-full"
></OperatorIcon>
)}
</TimelineIndicator>
</TimelineHeader>
</TimelineItem>
);
})}
</Timeline>
);
}

View File

@ -0,0 +1,114 @@
import { SkeletonCard } from '@/components/skeleton-card';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { IModalProps } from '@/interfaces/common';
import { cn } from '@/lib/utils';
import { PipelineResultSearchParams } from '@/pages/dataflow-result/constant';
import {
ArrowUpRight,
CirclePause,
Logs,
SquareArrowOutUpRight,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import 'react18-json-view/src/style.css';
import { useParams } from 'umi';
import {
isEndOutputEmpty,
useDownloadOutput,
} from '../hooks/use-download-output';
import { UseFetchLogReturnType } from '../hooks/use-fetch-pipeline-log';
import { DataflowTimeline } from './dataflow-timeline';
type LogSheetProps = IModalProps<any> & {
handleCancel(): void;
uploadedFileData?: Record<string, any>;
} & Pick<
UseFetchLogReturnType,
'isCompleted' | 'isLogEmpty' | 'isParsing' | 'logs' | 'messageId'
>;
export function PipelineLogSheet({
hideModal,
isParsing,
logs,
handleCancel,
isCompleted,
isLogEmpty,
messageId,
uploadedFileData,
}: LogSheetProps) {
const { t } = useTranslation();
const { id } = useParams();
const { data: agent } = useFetchAgent();
const { handleDownloadJson } = useDownloadOutput(logs);
const { navigateToDataflowResult } = useNavigatePage();
return (
<Sheet open onOpenChange={hideModal} modal={false}>
<SheetContent
className={cn('top-20 h-auto flex flex-col p-0 gap-0')}
onInteractOutside={(e) => e.preventDefault()}
>
<SheetHeader className="p-5">
<SheetTitle className="flex items-center gap-2.5">
<Logs className="size-4" /> {t('flow.log')}
{isCompleted && (
<Button
variant={'ghost'}
onClick={navigateToDataflowResult({
id: messageId, // 'log_id',
[PipelineResultSearchParams.AgentId]: id, // 'agent_id',
[PipelineResultSearchParams.DocumentId]: uploadedFileData?.id, //'doc_id',
[PipelineResultSearchParams.AgentTitle]: agent.title, //'title',
[PipelineResultSearchParams.IsReadOnly]: 'true',
[PipelineResultSearchParams.Type]: 'dataflow',
[PipelineResultSearchParams.CreatedBy]:
uploadedFileData?.created_by,
[PipelineResultSearchParams.DocumentExtension]:
uploadedFileData?.extension,
})}
>
{t('dataflow.viewResult')} <ArrowUpRight />
</Button>
)}
</SheetTitle>
</SheetHeader>
<section className="flex-1 overflow-auto px-5 pt-5">
{isLogEmpty ? (
<SkeletonCard className="mt-2" />
) : (
<DataflowTimeline traceList={logs}></DataflowTimeline>
)}
</section>
<div className="px-5 pb-5">
{isParsing ? (
<Button
className="w-full mt-8 bg-state-error/10 text-state-error hover:bg-state-error hover:text-bg-base"
onClick={handleCancel}
>
<CirclePause /> {t('dataflow.cancel')}
</Button>
) : (
<Button
onClick={handleDownloadJson}
disabled={isEndOutputEmpty(logs)}
className="w-full mt-8 bg-accent-primary-5 text-text-secondary hover:bg-accent-primary-5 hover:text-accent-primary hover:border-accent-primary hover:border"
>
<SquareArrowOutUpRight />
{t('dataflow.exportJson')}
</Button>
)}
</div>
</SheetContent>
</Sheet>
);
}

View File

@ -14,6 +14,7 @@ import pipe from 'lodash/fp/pipe';
import isObject from 'lodash/isObject';
import {
CategorizeAnchorPointPositions,
NoCopyOperatorsList,
NoDebugOperatorsList,
NodeHandleId,
Operator,
@ -403,6 +404,10 @@ export const needsSingleStepDebugging = (label: string) => {
return !NoDebugOperatorsList.some((x) => (label as Operator) === x);
};
export function showCopyIcon(label: string) {
return !NoCopyOperatorsList.some((x) => (label as Operator) === x);
}
// Get the coordinates of the node relative to the Iteration node
export function getRelativePositionToIterationNode(
nodes: RAGFlowNodeType[],
@ -464,7 +469,9 @@ export function convertToStringArray(
return list.map((x) => x.value);
}
export function convertToObjectArray(list: Array<string | number | boolean>) {
export function convertToObjectArray<T extends string | number | boolean>(
list: Array<T>,
) {
if (!Array.isArray(list)) {
return [];
}
@ -488,7 +495,7 @@ export const buildCategorizeListFromObject = (
// Categorize's to field has two data sources, with edges as the data source.
// Changes in the edge or to field need to be synchronized to the form field.
return Object.keys(categorizeItem)
.reduce<Array<ICategorizeItem>>((pre, cur) => {
.reduce<Array<Omit<ICategorizeItem, 'uuid'>>>((pre, cur) => {
// synchronize edge data to the to field
pre.push({

View File

@ -14,7 +14,7 @@ export type DatasetCardProps = {
} & Pick<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'>;
export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
const { navigateToAgent, navigateToDataflow } = useNavigatePage();
const { navigateToAgent } = useNavigatePage();
return (
<HomeCard
@ -26,9 +26,10 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
}
sharedBadge={<SharedBadge>{data.nickname}</SharedBadge>}
onClick={
data.canvas_category === AgentCategory.DataflowCanvas
? navigateToDataflow(data.id)
: navigateToAgent(data?.id)
// data.canvas_category === AgentCategory.DataflowCanvas
// ? navigateToDataflow(data.id)
// :
navigateToAgent(data?.id, data.canvas_category as AgentCategory)
}
icon={
data.canvas_category === AgentCategory.DataflowCanvas && (

View File

@ -1,26 +1,16 @@
import { IBeginNode } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { NodeProps, Position } from '@xyflow/react';
import get from 'lodash/get';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
BeginQueryType,
BeginQueryTypeIconMap,
NodeHandleId,
Operator,
} from '../../constant';
import { BeginQuery } from '../../interface';
import { NodeHandleId, Operator } from '../../constant';
import OperatorIcon from '../../operator-icon';
import { CommonHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import styles from './index.less';
import { NodeWrapper } from './node-wrapper';
// TODO: do not allow other nodes to connect to this node
function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
const { t } = useTranslation();
const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {});
return (
<NodeWrapper selected={selected}>
@ -39,22 +29,6 @@ function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
{t(`dataflow.begin`)}
</div>
</section>
<section className={cn(styles.generateParameters, 'flex gap-2 flex-col')}>
{Object.entries(inputs).map(([key, val], idx) => {
const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType];
return (
<div
key={idx}
className={cn(styles.conditionBlock, 'flex gap-1.5 items-center')}
>
<Icon className="size-4" />
<label htmlFor="">{key}</label>
<span className={styles.parameterValue}>{val.name}</span>
<span className="flex-1">{val.optional ? 'Yes' : 'No'}</span>
</div>
);
})}
</section>
</NodeWrapper>
);
}

View File

@ -3,24 +3,8 @@ import {
initialLlmBaseValues,
DataflowOperator as Operator,
} from '@/constants/agent';
import {
ChatVariableEnabledField,
variableEnabledFieldMap,
} from '@/constants/chat';
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
export { DataflowOperator as Operator } from '@/constants/agent';
import {
Circle,
CircleSlash2,
CloudUpload,
ListOrdered,
OptionIcon,
TextCursorInput,
ToggleLeft,
WrapText,
} from 'lucide-react';
export enum FileType {
PDF = 'pdf',
Spreadsheet = 'spreadsheet',
@ -104,47 +88,8 @@ export enum ContextGeneratorFieldName {
Metadata = 'metadata',
}
export enum PromptRole {
User = 'user',
Assistant = 'assistant',
}
export enum AgentDialogueMode {
Conversational = 'conversational',
Task = 'task',
}
export const BeginId = 'File';
export const SwitchLogicOperatorOptions = ['and', 'or'];
export const CommonOperatorList = Object.values(Operator).filter(
(x) => x !== Operator.Note,
);
export const SwitchOperatorOptions = [
{ value: '=', label: 'equal', icon: 'equal' },
{ value: '≠', label: 'notEqual', icon: 'not-equals' },
{ value: '>', label: 'gt', icon: 'Less' },
{ value: '≥', label: 'ge', icon: 'Greater-or-equal' },
{ value: '<', label: 'lt', icon: 'Less' },
{ value: '≤', label: 'le', icon: 'less-or-equal' },
{ value: 'contains', label: 'contains', icon: 'Contains' },
{ value: 'not contains', label: 'notContains', icon: 'not-contains' },
{ value: 'start with', label: 'startWith', icon: 'list-start' },
{ value: 'end with', label: 'endWith', icon: 'list-end' },
{
value: 'empty',
label: 'empty',
icon: <Circle className="size-4" />,
},
{
value: 'not empty',
label: 'notEmpty',
icon: <CircleSlash2 className="size-4" />,
},
];
export const SwitchElseTo = 'end_cpn_ids';
export enum TokenizerSearchMethod {
@ -186,15 +131,6 @@ export const initialBeginValues = {
},
};
export const variableCheckBoxFieldMap = Object.keys(
variableEnabledFieldMap,
).reduce<Record<string, boolean>>((pre, cur) => {
pre[cur] = setInitialChatVariableEnabledFieldValue(
cur as ChatVariableEnabledField,
);
return pre;
}, {});
export const initialNoteValues = {
text: '',
};
@ -341,24 +277,6 @@ export const NodeMap = {
[Operator.Extractor]: 'contextNode',
};
export enum BeginQueryType {
Line = 'line',
Paragraph = 'paragraph',
Options = 'options',
File = 'file',
Integer = 'integer',
Boolean = 'boolean',
}
export const BeginQueryTypeIconMap = {
[BeginQueryType.Line]: TextCursorInput,
[BeginQueryType.Paragraph]: WrapText,
[BeginQueryType.Options]: OptionIcon,
[BeginQueryType.File]: CloudUpload,
[BeginQueryType.Integer]: ListOrdered,
[BeginQueryType.Boolean]: ToggleLeft,
};
export const NoDebugOperatorsList = [Operator.Begin];
export enum NodeHandleId {
@ -370,17 +288,6 @@ export enum NodeHandleId {
AgentException = 'agentException',
}
export enum VariableType {
String = 'string',
Array = 'array',
File = 'file',
}
export enum AgentExceptionMethod {
Comment = 'comment',
Goto = 'goto',
}
export const FileTypeSuffixMap = {
[FileType.PDF]: ['pdf'],
[FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'],