mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-26 17:16:52 +08:00
### 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:
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
196
web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx
Normal file
196
web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
100
web/src/pages/agent/canvas/node/dropdown/operator-item-list.tsx
Normal file
100
web/src/pages/agent/canvas/node/dropdown/operator-item-list.tsx
Normal 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>;
|
||||
}
|
||||
18
web/src/pages/agent/canvas/node/extractor-node.tsx
Normal file
18
web/src/pages/agent/canvas/node/extractor-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
62
web/src/pages/agent/canvas/node/file-node.tsx
Normal file
62
web/src/pages/agent/canvas/node/file-node.tsx
Normal 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);
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
57
web/src/pages/agent/canvas/node/parser-node.tsx
Normal file
57
web/src/pages/agent/canvas/node/parser-node.tsx
Normal 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);
|
||||
58
web/src/pages/agent/canvas/node/splitter-node.tsx
Normal file
58
web/src/pages/agent/canvas/node/splitter-node.tsx
Normal 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);
|
||||
55
web/src/pages/agent/canvas/node/tokenizer-node.tsx
Normal file
55
web/src/pages/agent/canvas/node/tokenizer-node.tsx
Normal 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);
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user