diff --git a/web/src/constants/agent.ts b/web/src/constants/agent.ts index 6e388d7a0..f6af4f57c 100644 --- a/web/src/constants/agent.ts +++ b/web/src/constants/agent.ts @@ -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', +} diff --git a/web/src/hooks/logic-hooks/navigate-hooks.ts b/web/src/hooks/logic-hooks/navigate-hooks.ts index 042489bac..92f573010 100644 --- a/web/src/hooks/logic-hooks/navigate-hooks.ts +++ b/web/src/hooks/logic-hooks/navigate-hooks.ts @@ -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], ); diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx index 0bf32d08f..ec81bd9e6 100644 --- a/web/src/pages/agent/canvas/index.tsx +++ b/web/src/pages/agent/canvas/index.tsx @@ -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) { } } > - { removePlaceholderNode(); hideModal(); @@ -320,9 +331,10 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { }} position={dropdownPosition} onNodeCreated={onNodeCreated} + nodeId={nodeId} > - + )} diff --git a/web/src/pages/agent/canvas/node/card.tsx b/web/src/pages/agent/canvas/node/card.tsx index 042ca45e0..cd5e1ae09 100644 --- a/web/src/pages/agent/canvas/node/card.tsx +++ b/web/src/pages/agent/canvas/node/card.tsx @@ -17,6 +17,9 @@ import { SelectValue, } from '@/components/ui/select'; +import { cn } from '@/lib/utils'; +import { PropsWithChildren } from 'react'; + export function CardWithForm() { return ( @@ -55,3 +58,13 @@ export function CardWithForm() { ); } + +type LabelCardProps = { + className?: string; +} & PropsWithChildren; + +export function LabelCard({ children, className }: LabelCardProps) { + return ( +
{children}
+ ); +} diff --git a/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx new file mode 100644 index 000000000..3ea40f986 --- /dev/null +++ b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx @@ -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 ( + + + + {t('flow.foundation')} + + + + + + + + {t('flow.dialog')} + + + + + + + + {t('flow.flow')} + + + + + + + + {t('flow.dataManipulation')} + + + + + + + + {t('flow.tools')} + + + + + + + ); +} + +// 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 ( + <> + + {showChunker && ( + + + Chunker + + + + + + )} + + ); +} diff --git a/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx b/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx index bc7bf4577..dd2945b77 100644 --- a/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx +++ b/web/src/pages/agent/canvas/node/dropdown/next-step-dropdown.tsx @@ -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['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 => - (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 = ( -
- - {t(`flow.${lowerFirst(operator)}`)} -
- ); - - return ( - - - {isCustomDropdown ? ( -
  • {commonContent}
  • - ) : ( - hideModal?.()} - > - - {t(`flow.${lowerFirst(operator)}`)} - - )} -
    - -

    {t(`flow.${lowerFirst(operator)}Description`)}

    -
    -
    - ); - }; - - return ; -} - -function AccordionOperators({ - isCustomDropdown = false, - mousePosition, -}: { - isCustomDropdown?: boolean; - mousePosition?: { x: number; y: number }; -}) { - return ( - - - - {t('flow.foundation')} - - - - - - - - {t('flow.dialog')} - - - - - - - - {t('flow.flow')} - - - - - - - - {t('flow.dataManipulation')} - - - - - - - - {t('flow.tools')} - - - - - - - ); -} + AccordionOperators, + PipelineAccordionOperators, +} from './accordion-operators'; +import { HideModalContext, OnNodeCreatedContext } from './operator-item-list'; export function InnerNextStepDropdown({ children, hideModal, position, onNodeCreated, + nodeId, }: PropsWithChildren & IModalProps & { position?: { x: number; y: number }; onNodeCreated?: (newNodeId: string) => void; + nodeId?: string; }) { const dropdownRef = useRef(null); + const isPipeline = useIsPipeline(); useEffect(() => { if (position && hideModal) { @@ -260,10 +63,18 @@ export function InnerNextStepDropdown({ - + {isPipeline ? ( + + ) : ( + + )} @@ -287,7 +98,11 @@ export function InnerNextStepDropdown({ > {t('flow.nextStep')} - + {isPipeline ? ( + + ) : ( + + )} diff --git a/web/src/pages/agent/canvas/node/dropdown/operator-item-list.tsx b/web/src/pages/agent/canvas/node/dropdown/operator-item-list.tsx new file mode 100644 index 000000000..30480bd50 --- /dev/null +++ b/web/src/pages/agent/canvas/node/dropdown/operator-item-list.tsx @@ -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['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 => + (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 = ( +
    + + {t(`flow.${lowerFirst(operator)}`)} +
    + ); + + return ( + + + {isCustomDropdown ? ( +
  • {commonContent}
  • + ) : ( + hideModal?.()} + > + + {t(`flow.${lowerFirst(operator)}`)} + + )} +
    + +

    {t(`flow.${lowerFirst(operator)}Description`)}

    +
    +
    + ); + }; + + return
      {operators.map(renderOperatorItem)}
    ; +} diff --git a/web/src/pages/agent/canvas/node/extractor-node.tsx b/web/src/pages/agent/canvas/node/extractor-node.tsx new file mode 100644 index 000000000..8b2e03483 --- /dev/null +++ b/web/src/pages/agent/canvas/node/extractor-node.tsx @@ -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) { + const { data } = props; + + return ( + + + + + + ); +} diff --git a/web/src/pages/agent/canvas/node/file-node.tsx b/web/src/pages/agent/canvas/node/file-node.tsx new file mode 100644 index 000000000..41e0b2507 --- /dev/null +++ b/web/src/pages/agent/canvas/node/file-node.tsx @@ -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) { + const { t } = useTranslation(); + const inputs: Record = get(data, 'form.inputs', {}); + + return ( + + + +
    + +
    + {t(`dataflow.begin`)} +
    +
    +
    + {Object.entries(inputs).map(([key, val], idx) => { + const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType]; + return ( +
    + + + {val.name} + {val.optional ? 'Yes' : 'No'} +
    + ); + })} +
    +
    + ); +} + +export const FileNode = memo(InnerFileNode); diff --git a/web/src/pages/agent/canvas/node/handle.tsx b/web/src/pages/agent/canvas/node/handle.tsx index 43ee94bc8..2f7530fd6 100644 --- a/web/src/pages/agent/canvas/node/handle.tsx +++ b/web/src/pages/agent/canvas/node/handle.tsx @@ -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({ > {visible && ( - { hideModal(); clearActiveDropdown(); }} > - + )} diff --git a/web/src/pages/agent/canvas/node/index.tsx b/web/src/pages/agent/canvas/node/index.tsx index ccd00b9ca..953bb6daa 100644 --- a/web/src/pages/agent/canvas/node/index.tsx +++ b/web/src/pages/agent/canvas/node/index.tsx @@ -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)} > diff --git a/web/src/pages/agent/canvas/node/node-header.tsx b/web/src/pages/agent/canvas/node/node-header.tsx index cada3deee..de5b222ed 100644 --- a/web/src/pages/agent/canvas/node/node-header.tsx +++ b/web/src/pages/agent/canvas/node/node-header.tsx @@ -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 (
    - + {icon || } {name} diff --git a/web/src/pages/agent/canvas/node/parser-node.tsx b/web/src/pages/agent/canvas/node/parser-node.tsx new file mode 100644 index 000000000..15539d0b8 --- /dev/null +++ b/web/src/pages/agent/canvas/node/parser-node.tsx @@ -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>) { + const { t } = useTranslation(); + return ( + + + + + + + {(x, idx) => ( + + Parser {idx + 1} + {t(`dataflow.fileFormatOptions.${x.fileFormat}`)} + + )} + + + ); +} + +export default memo(ParserNode); diff --git a/web/src/pages/agent/canvas/node/splitter-node.tsx b/web/src/pages/agent/canvas/node/splitter-node.tsx new file mode 100644 index 000000000..5797bd5e3 --- /dev/null +++ b/web/src/pages/agent/canvas/node/splitter-node.tsx @@ -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 & PropsWithChildren; +function InnerSplitterNode({ + id, + data, + isConnectable = true, + selected, +}: RagNodeProps) { + return ( + + + + + } + > + {data.name} + + + ); +} + +export const SplitterNode = memo(InnerSplitterNode); diff --git a/web/src/pages/agent/canvas/node/tokenizer-node.tsx b/web/src/pages/agent/canvas/node/tokenizer-node.tsx new file mode 100644 index 000000000..20b261bf4 --- /dev/null +++ b/web/src/pages/agent/canvas/node/tokenizer-node.tsx @@ -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>) { + const { t } = useTranslation(); + + return ( + + + + + + + {t('dataflow.searchMethod')} + +
      + {data.form?.search_method.map((x) => ( +
    • {t(`dataflow.tokenizerSearchMethodOptions.${x}`)}
    • + ))} +
    +
    +
    +
    + ); +} + +export default memo(TokenizerNode); diff --git a/web/src/pages/agent/canvas/node/toolbar.tsx b/web/src/pages/agent/canvas/node/toolbar.tsx index f36cfe2af..565378575 100644 --- a/web/src/pages/agent/canvas/node/toolbar.tsx +++ b/web/src/pages/agent/canvas/node/toolbar.tsx @@ -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({ - )}{' '} - - - + )} + {showCopy && ( + + + + )} diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant/index.tsx similarity index 94% rename from web/src/pages/agent/constant.tsx rename to web/src/pages/agent/constant/index.tsx index 35a67cae4..c444b830e 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant/index.tsx @@ -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 { diff --git a/web/src/pages/agent/constant/pipeline.tsx b/web/src/pages/agent/constant/pipeline.tsx new file mode 100644 index 000000000..78d4b3ac9 --- /dev/null +++ b/web/src/pages/agent/constant/pipeline.tsx @@ -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', 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', 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', 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', 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, +]; diff --git a/web/src/pages/agent/context.ts b/web/src/pages/agent/context.ts index 6839554d3..9d575cc3c 100644 --- a/web/src/pages/agent/context.ts +++ b/web/src/pages/agent/context.ts @@ -48,3 +48,13 @@ export type HandleContextType = { export const HandleContext = createContext( {} as HandleContextType, ); + +export type PipelineLogContextType = { + messageId: string; + setMessageId: (messageId: string) => void; + setUploadedFileData: (data: Record) => void; +}; + +export const PipelineLogContext = createContext( + {} as PipelineLogContextType, +); diff --git a/web/src/pages/agent/form-sheet/form-config-map.tsx b/web/src/pages/agent/form-sheet/form-config-map.tsx index 15ca5ffa3..f99e2eed7 100644 --- a/web/src/pages/agent/form-sheet/form-config-map.tsx +++ b/web/src/pages/agent/form-sheet/form-config-map.tsx @@ -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, + }, }; diff --git a/web/src/pages/agent/form/extractor-form/index.tsx b/web/src/pages/agent/form/extractor-form/index.tsx new file mode 100644 index 000000000..c178e8a46 --- /dev/null +++ b/web/src/pages/agent/form/extractor-form/index.tsx @@ -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; + +const outputList = buildOutputList(initialExtractorValues.outputs); + +const ExtractorForm = ({ node }: INextOperatorForm) => { + const defaultValues = useFormValues(initialExtractorValues, node); + const { t } = useTranslation(); + + const form = useForm({ + 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 ( +
    + + + + {(field) => ( + { + field.onChange(value); + handleFieldNameChange(value); + }} + value={field.value} + placeholder={t('dataFlowPlaceholder')} + options={options} + > + )} + + + + + + + + + + {visible && ( + + )} +
    + ); +}; + +export default memo(ExtractorForm); diff --git a/web/src/pages/agent/form/extractor-form/use-switch-prompt.ts b/web/src/pages/agent/form/extractor-form/use-switch-prompt.ts new file mode 100644 index 000000000..4efb2c472 --- /dev/null +++ b/web/src/pages/agent/form/extractor-form/use-switch-prompt.ts @@ -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; + +export function useSwitchPrompt(form: UseFormReturn) { + const { visible, showModal, hideModal } = useSetModalState(); + const { t } = useTranslation(); + const previousFieldNames = useRef([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, + }; +} diff --git a/web/src/pages/agent/form/hierarchical-merger-form/index.tsx b/web/src/pages/agent/form/hierarchical-merger-form/index.tsx new file mode 100644 index 000000000..623530792 --- /dev/null +++ b/web/src/pages/agent/form/hierarchical-merger-form/index.tsx @@ -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; + +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 ( + + + H{index + 1} + {isLatest && ( + + )} + + + + {t('dataflow.regularExpressions')} + +
    + {fields.map((field, index) => ( +
    +
    + + + +
    + {index === 0 ? ( + + ) : ( + + )} +
    + ))} +
    +
    +
    + ); +} + +const HierarchicalMergerForm = ({ node }: INextOperatorForm) => { + const { t } = useTranslation(); + const defaultValues = useFormValues(initialHierarchicalMergerValues, node); + + const form = useForm({ + defaultValues, + resolver: zodResolver(FormSchema), + mode: 'onChange', + }); + + const name = 'levels'; + + const { fields, append, remove } = useFieldArray({ + name: name, + control: form.control, + }); + + useWatchFormChange(node?.id, form); + + return ( +
    + + + + + {fields.map((field, index) => ( +
    +
    + +
    +
    + ))} + {fields.length < 5 && ( + append({ expressions: [{ expression: '' }] })} + > + {t('common.add')} + + )} +
    +
    + +
    +
    + ); +}; + +export default memo(HierarchicalMergerForm); diff --git a/web/src/pages/agent/form/parser-form/common-form-fields.tsx b/web/src/pages/agent/form/parser-form/common-form-fields.tsx new file mode 100644 index 000000000..a44b22a83 --- /dev/null +++ b/web/src/pages/agent/form/parser-form/common-form-fields.tsx @@ -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 + >((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 ( + + + + ); +} + +export function ParserMethodFormField({ + prefix, + optionsWithoutLLM, +}: CommonProps & { optionsWithoutLLM?: { value: string; label: string }[] }) { + const { t } = useTranslation(); + return ( + + ); +} + +export function LargeModelFormField({ + prefix, + options, +}: CommonProps & Pick) { + return ( + + ); +} + +export function LanguageFormField({ prefix }: CommonProps) { + const { t } = useTranslation(); + + return ( + + {(field) => ( + + )} + + ); +} diff --git a/web/src/pages/agent/form/parser-form/email-form-fields.tsx b/web/src/pages/agent/form/parser-form/email-form-fields.tsx new file mode 100644 index 000000000..fef3f1c52 --- /dev/null +++ b/web/src/pages/agent/form/parser-form/email-form-fields.tsx @@ -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 ( + <> + + {(field) => ( + + )} + + + ); +} diff --git a/web/src/pages/agent/form/parser-form/image-form-fields.tsx b/web/src/pages/agent/form/parser-form/image-form-fields.tsx new file mode 100644 index 000000000..c5dc83a70 --- /dev/null +++ b/web/src/pages/agent/form/parser-form/image-form-fields.tsx @@ -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 ( + <> + + {languageShown && } + {languageShown && ( + +