mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Feat: Initialize the data pipeline canvas. #9869 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
18
web/src/pages/data-flow/canvas/context-menu/index.less
Normal file
18
web/src/pages/data-flow/canvas/context-menu/index.less
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.contextMenu {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-style: solid;
|
||||||
|
box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
padding: 0.5em;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
107
web/src/pages/data-flow/canvas/context-menu/index.tsx
Normal file
107
web/src/pages/data-flow/canvas/context-menu/index.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { NodeMouseHandler, useReactFlow } from '@xyflow/react';
|
||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
export interface INodeContextMenu {
|
||||||
|
id: string;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
right?: number;
|
||||||
|
bottom?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NodeContextMenu({
|
||||||
|
id,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
...props
|
||||||
|
}: INodeContextMenu) {
|
||||||
|
const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
|
||||||
|
|
||||||
|
const duplicateNode = useCallback(() => {
|
||||||
|
const node = getNode(id);
|
||||||
|
const position = {
|
||||||
|
x: node?.position?.x || 0 + 50,
|
||||||
|
y: node?.position?.y || 0 + 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
addNodes({
|
||||||
|
...(node || {}),
|
||||||
|
data: node?.data,
|
||||||
|
selected: false,
|
||||||
|
dragging: false,
|
||||||
|
id: `${node?.id}-copy`,
|
||||||
|
position,
|
||||||
|
});
|
||||||
|
}, [id, getNode, addNodes]);
|
||||||
|
|
||||||
|
const deleteNode = useCallback(() => {
|
||||||
|
setNodes((nodes) => nodes.filter((node) => node.id !== id));
|
||||||
|
setEdges((edges) => edges.filter((edge) => edge.source !== id));
|
||||||
|
}, [id, setNodes, setEdges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ top, left, right, bottom }}
|
||||||
|
className={styles.contextMenu}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<p style={{ margin: '0.5em' }}>
|
||||||
|
<small>node: {id}</small>
|
||||||
|
</p>
|
||||||
|
<button onClick={duplicateNode} type={'button'}>
|
||||||
|
duplicate
|
||||||
|
</button>
|
||||||
|
<button onClick={deleteNode} type={'button'}>
|
||||||
|
delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @deprecated
|
||||||
|
*/
|
||||||
|
export const useHandleNodeContextMenu = (sideWidth: number) => {
|
||||||
|
const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu);
|
||||||
|
const ref = useRef<any>(null);
|
||||||
|
|
||||||
|
const onNodeContextMenu: NodeMouseHandler = useCallback(
|
||||||
|
(event, node) => {
|
||||||
|
// Prevent native context menu from showing
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Calculate position of the context menu. We want to make sure it
|
||||||
|
// doesn't get positioned off-screen.
|
||||||
|
const pane = ref.current?.getBoundingClientRect();
|
||||||
|
// setMenu({
|
||||||
|
// id: node.id,
|
||||||
|
// top: event.clientY < pane.height - 200 ? event.clientY : 0,
|
||||||
|
// left: event.clientX < pane.width - 200 ? event.clientX : 0,
|
||||||
|
// right: event.clientX >= pane.width - 200 ? pane.width - event.clientX : 0,
|
||||||
|
// bottom:
|
||||||
|
// event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
|
||||||
|
// });
|
||||||
|
|
||||||
|
setMenu({
|
||||||
|
id: node.id,
|
||||||
|
top: event.clientY - 144,
|
||||||
|
left: event.clientX - sideWidth,
|
||||||
|
// top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0,
|
||||||
|
// left: event.clientX < pane.width - 200 ? event.clientX : 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[sideWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close the context menu if it's open whenever the window is clicked.
|
||||||
|
const onPaneClick = useCallback(
|
||||||
|
() => setMenu({} as INodeContextMenu),
|
||||||
|
[setMenu],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { onNodeContextMenu, menu, onPaneClick, ref };
|
||||||
|
};
|
||||||
56
web/src/pages/data-flow/canvas/context.tsx
Normal file
56
web/src/pages/data-flow/canvas/context.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
interface DropdownContextType {
|
||||||
|
canShowDropdown: () => boolean;
|
||||||
|
setActiveDropdown: (type: 'handle' | 'drag') => void;
|
||||||
|
clearActiveDropdown: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownContext = createContext<DropdownContextType | null>(null);
|
||||||
|
|
||||||
|
export const useDropdownManager = () => {
|
||||||
|
const context = useContext(DropdownContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useDropdownManager must be used within DropdownProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DropdownProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DropdownProvider = ({ children }: DropdownProviderProps) => {
|
||||||
|
const activeDropdownRef = useRef<'handle' | 'drag' | null>(null);
|
||||||
|
|
||||||
|
const canShowDropdown = useCallback(() => {
|
||||||
|
const current = activeDropdownRef.current;
|
||||||
|
return !current;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setActiveDropdown = useCallback((type: 'handle' | 'drag') => {
|
||||||
|
activeDropdownRef.current = type;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearActiveDropdown = useCallback(() => {
|
||||||
|
activeDropdownRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value: DropdownContextType = {
|
||||||
|
canShowDropdown,
|
||||||
|
setActiveDropdown,
|
||||||
|
clearActiveDropdown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</DropdownContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
126
web/src/pages/data-flow/canvas/edge/index.tsx
Normal file
126
web/src/pages/data-flow/canvas/edge/index.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
BaseEdge,
|
||||||
|
Edge,
|
||||||
|
EdgeLabelRenderer,
|
||||||
|
EdgeProps,
|
||||||
|
getBezierPath,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
|
||||||
|
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { NodeHandleId, Operator } from '../../constant';
|
||||||
|
|
||||||
|
function InnerButtonEdge({
|
||||||
|
id,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
sourcePosition,
|
||||||
|
targetPosition,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
style = {},
|
||||||
|
markerEnd,
|
||||||
|
selected,
|
||||||
|
data,
|
||||||
|
sourceHandleId,
|
||||||
|
}: EdgeProps<Edge<{ isHovered: boolean }>>) {
|
||||||
|
const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
|
||||||
|
const [edgePath, labelX, labelY] = getBezierPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition,
|
||||||
|
});
|
||||||
|
const selectedStyle = useMemo(() => {
|
||||||
|
return selected ? { strokeWidth: 1, stroke: 'rgba(76, 164, 231, 1)' } : {};
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
|
const onEdgeClick = () => {
|
||||||
|
deleteEdgeById(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// highlight the nodes that the workflow passes through
|
||||||
|
const { data: flowDetail } = useFetchAgent();
|
||||||
|
|
||||||
|
const graphPath = useMemo(() => {
|
||||||
|
// TODO: this will be called multiple times
|
||||||
|
const path = flowDetail?.dsl?.path ?? [];
|
||||||
|
// The second to last
|
||||||
|
const previousGraphPath: string[] = path.at(-2) ?? [];
|
||||||
|
let graphPath: string[] = path.at(-1) ?? [];
|
||||||
|
// The last of the second to last article
|
||||||
|
const previousLatestElement = previousGraphPath.at(-1);
|
||||||
|
if (previousGraphPath.length > 0 && previousLatestElement) {
|
||||||
|
graphPath = [previousLatestElement, ...graphPath];
|
||||||
|
}
|
||||||
|
return Array.isArray(graphPath) ? graphPath : [];
|
||||||
|
}, [flowDetail.dsl?.path]);
|
||||||
|
|
||||||
|
const highlightStyle = useMemo(() => {
|
||||||
|
const idx = graphPath.findIndex((x) => x === source);
|
||||||
|
if (idx !== -1) {
|
||||||
|
// The set of elements following source
|
||||||
|
const slicedGraphPath = graphPath.slice(idx + 1);
|
||||||
|
if (slicedGraphPath.some((x) => x === target)) {
|
||||||
|
return { strokeWidth: 1, stroke: 'red' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}, [source, target, graphPath]);
|
||||||
|
|
||||||
|
const visible = useMemo(() => {
|
||||||
|
return (
|
||||||
|
data?.isHovered &&
|
||||||
|
sourceHandleId !== NodeHandleId.Tool &&
|
||||||
|
sourceHandleId !== NodeHandleId.AgentBottom && // The connection between the agent node and the tool node does not need to display the delete button
|
||||||
|
!target.startsWith(Operator.Tool)
|
||||||
|
);
|
||||||
|
}, [data?.isHovered, sourceHandleId, target]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge
|
||||||
|
path={edgePath}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
style={{ ...style, ...selectedStyle, ...highlightStyle }}
|
||||||
|
className="text-text-secondary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
fontSize: 12,
|
||||||
|
// everything inside EdgeLabelRenderer has no pointer events by default
|
||||||
|
// if you have an interactive element, set pointer-events: all
|
||||||
|
pointerEvents: 'all',
|
||||||
|
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
|
||||||
|
}}
|
||||||
|
className="nodrag nopan"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'size-3.5 border border-state-error text-state-error rounded-full leading-none',
|
||||||
|
'invisible',
|
||||||
|
{ visible },
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
onClick={onEdgeClick}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonEdge = memo(InnerButtonEdge);
|
||||||
11
web/src/pages/data-flow/canvas/index.less
Normal file
11
web/src/pages/data-flow/canvas/index.less
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.canvasWrapper {
|
||||||
|
position: relative;
|
||||||
|
height: calc(100% - 64px);
|
||||||
|
:global(.react-flow__node-group) {
|
||||||
|
.commonNode();
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
331
web/src/pages/data-flow/canvas/index.tsx
Normal file
331
web/src/pages/data-flow/canvas/index.tsx
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
import { useIsDarkTheme, useTheme } from '@/components/theme-provider';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
Connection,
|
||||||
|
ConnectionMode,
|
||||||
|
ControlButton,
|
||||||
|
Controls,
|
||||||
|
NodeTypes,
|
||||||
|
Position,
|
||||||
|
ReactFlow,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
import '@xyflow/react/dist/style.css';
|
||||||
|
import { NotebookPen } from 'lucide-react';
|
||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AgentBackground } from '../components/background';
|
||||||
|
import { AgentInstanceContext, HandleContext } from '../context';
|
||||||
|
|
||||||
|
import FormSheet from '../form-sheet/next';
|
||||||
|
import {
|
||||||
|
useHandleDrop,
|
||||||
|
useSelectCanvasData,
|
||||||
|
useValidateConnection,
|
||||||
|
} from '../hooks';
|
||||||
|
import { useAddNode } from '../hooks/use-add-node';
|
||||||
|
import { useBeforeDelete } from '../hooks/use-before-delete';
|
||||||
|
import { useMoveNote } from '../hooks/use-move-note';
|
||||||
|
import { useDropdownManager } from './context';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useHideFormSheetOnNodeDeletion,
|
||||||
|
useShowDrawer,
|
||||||
|
} from '../hooks/use-show-drawer';
|
||||||
|
import RunSheet from '../run-sheet';
|
||||||
|
import { ButtonEdge } from './edge';
|
||||||
|
import styles from './index.less';
|
||||||
|
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 { 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 { LogicNode } from './node/logic-node';
|
||||||
|
import { MessageNode } from './node/message-node';
|
||||||
|
import NoteNode from './node/note-node';
|
||||||
|
import { RelevantNode } from './node/relevant-node';
|
||||||
|
import { RetrievalNode } from './node/retrieval-node';
|
||||||
|
import { RewriteNode } from './node/rewrite-node';
|
||||||
|
import { SwitchNode } from './node/switch-node';
|
||||||
|
import { TemplateNode } from './node/template-node';
|
||||||
|
import { ToolNode } from './node/tool-node';
|
||||||
|
|
||||||
|
export const nodeTypes: NodeTypes = {
|
||||||
|
ragNode: RagNode,
|
||||||
|
categorizeNode: CategorizeNode,
|
||||||
|
beginNode: BeginNode,
|
||||||
|
relevantNode: RelevantNode,
|
||||||
|
logicNode: LogicNode,
|
||||||
|
noteNode: NoteNode,
|
||||||
|
switchNode: SwitchNode,
|
||||||
|
generateNode: GenerateNode,
|
||||||
|
retrievalNode: RetrievalNode,
|
||||||
|
messageNode: MessageNode,
|
||||||
|
rewriteNode: RewriteNode,
|
||||||
|
keywordNode: KeywordNode,
|
||||||
|
invokeNode: InvokeNode,
|
||||||
|
templateNode: TemplateNode,
|
||||||
|
// emailNode: EmailNode,
|
||||||
|
group: IterationNode,
|
||||||
|
iterationStartNode: IterationStartNode,
|
||||||
|
agentNode: AgentNode,
|
||||||
|
toolNode: ToolNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const edgeTypes = {
|
||||||
|
buttonEdge: ButtonEdge,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
drawerVisible: boolean;
|
||||||
|
hideDrawer(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
onConnect: originalOnConnect,
|
||||||
|
onEdgesChange,
|
||||||
|
onNodesChange,
|
||||||
|
onSelectionChange,
|
||||||
|
onEdgeMouseEnter,
|
||||||
|
onEdgeMouseLeave,
|
||||||
|
} = useSelectCanvasData();
|
||||||
|
const isValidConnection = useValidateConnection();
|
||||||
|
|
||||||
|
const { onDrop, onDragOver, setReactFlowInstance, reactFlowInstance } =
|
||||||
|
useHandleDrop();
|
||||||
|
|
||||||
|
const {
|
||||||
|
onNodeClick,
|
||||||
|
clickedNode,
|
||||||
|
formDrawerVisible,
|
||||||
|
hideFormDrawer,
|
||||||
|
singleDebugDrawerVisible,
|
||||||
|
hideSingleDebugDrawer,
|
||||||
|
showSingleDebugDrawer,
|
||||||
|
chatVisible,
|
||||||
|
runVisible,
|
||||||
|
hideRunOrChatDrawer,
|
||||||
|
showChatModal,
|
||||||
|
showFormDrawer,
|
||||||
|
} = useShowDrawer({
|
||||||
|
drawerVisible,
|
||||||
|
hideDrawer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleBeforeDelete } = useBeforeDelete();
|
||||||
|
|
||||||
|
const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance);
|
||||||
|
|
||||||
|
const { ref, showImage, hideImage, imgVisible, mouse } = useMoveNote();
|
||||||
|
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
const isDarkTheme = useIsDarkTheme();
|
||||||
|
|
||||||
|
useHideFormSheetOnNodeDeletion({ hideFormDrawer });
|
||||||
|
|
||||||
|
const { visible, hideModal, showModal } = useSetModalState();
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const isConnectedRef = useRef(false);
|
||||||
|
const connectionStartRef = useRef<{
|
||||||
|
nodeId: string;
|
||||||
|
handleId: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const preventCloseRef = useRef(false);
|
||||||
|
|
||||||
|
const { setActiveDropdown, clearActiveDropdown } = useDropdownManager();
|
||||||
|
|
||||||
|
const onPaneClick = useCallback(() => {
|
||||||
|
hideFormDrawer();
|
||||||
|
if (visible && !preventCloseRef.current) {
|
||||||
|
hideModal();
|
||||||
|
clearActiveDropdown();
|
||||||
|
}
|
||||||
|
if (imgVisible) {
|
||||||
|
addNoteNode(mouse);
|
||||||
|
hideImage();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
hideFormDrawer,
|
||||||
|
visible,
|
||||||
|
hideModal,
|
||||||
|
imgVisible,
|
||||||
|
addNoteNode,
|
||||||
|
mouse,
|
||||||
|
hideImage,
|
||||||
|
clearActiveDropdown,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onConnect = (connection: Connection) => {
|
||||||
|
originalOnConnect(connection);
|
||||||
|
isConnectedRef.current = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OnConnectStart = (event: any, params: any) => {
|
||||||
|
isConnectedRef.current = false;
|
||||||
|
|
||||||
|
if (params && params.nodeId && params.handleId) {
|
||||||
|
connectionStartRef.current = {
|
||||||
|
nodeId: params.nodeId,
|
||||||
|
handleId: params.handleId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
connectionStartRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const OnConnectEnd = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if ('clientX' in event && 'clientY' in event) {
|
||||||
|
const { clientX, clientY } = event;
|
||||||
|
setDropdownPosition({ x: clientX, y: clientY });
|
||||||
|
if (!isConnectedRef.current) {
|
||||||
|
setActiveDropdown('drag');
|
||||||
|
showModal();
|
||||||
|
preventCloseRef.current = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
preventCloseRef.current = false;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.canvasWrapper}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{ position: 'absolute', top: 10, left: 0 }}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<marker
|
||||||
|
fill="rgb(157 149 225)"
|
||||||
|
id="logo"
|
||||||
|
viewBox="0 0 40 40"
|
||||||
|
refX="8"
|
||||||
|
refY="5"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
markerWidth="20"
|
||||||
|
markerHeight="20"
|
||||||
|
orient="auto-start-reverse"
|
||||||
|
>
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<AgentInstanceContext.Provider value={{ addCanvasNode, showFormDrawer }}>
|
||||||
|
<ReactFlow
|
||||||
|
connectionMode={ConnectionMode.Loose}
|
||||||
|
nodes={nodes}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
edges={edges}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
fitView
|
||||||
|
onConnect={onConnect}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onConnectStart={OnConnectStart}
|
||||||
|
onConnectEnd={OnConnectEnd}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
onPaneClick={onPaneClick}
|
||||||
|
onInit={setReactFlowInstance}
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
|
nodeOrigin={[0.5, 0]}
|
||||||
|
isValidConnection={isValidConnection}
|
||||||
|
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||||
|
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||||
|
className="h-full"
|
||||||
|
colorMode={theme}
|
||||||
|
defaultEdgeOptions={{
|
||||||
|
type: 'buttonEdge',
|
||||||
|
markerEnd: 'logo',
|
||||||
|
style: {
|
||||||
|
strokeWidth: 1,
|
||||||
|
stroke: isDarkTheme
|
||||||
|
? 'rgba(91, 93, 106, 1)'
|
||||||
|
: 'rgba(151, 154, 171, 1)',
|
||||||
|
},
|
||||||
|
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
|
||||||
|
}}
|
||||||
|
deleteKeyCode={['Delete', 'Backspace']}
|
||||||
|
onBeforeDelete={handleBeforeDelete}
|
||||||
|
>
|
||||||
|
<AgentBackground></AgentBackground>
|
||||||
|
<Controls position={'bottom-center'} orientation="horizontal">
|
||||||
|
<ControlButton>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<NotebookPen className="!fill-none" onClick={showImage} />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t('flow.note')}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</ControlButton>
|
||||||
|
</Controls>
|
||||||
|
</ReactFlow>
|
||||||
|
{visible && (
|
||||||
|
<HandleContext.Provider
|
||||||
|
value={{
|
||||||
|
nodeId: connectionStartRef.current?.nodeId || '',
|
||||||
|
id: connectionStartRef.current?.handleId || '',
|
||||||
|
type: 'source',
|
||||||
|
position: Position.Right,
|
||||||
|
isFromConnectionDrag: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InnerNextStepDropdown
|
||||||
|
hideModal={() => {
|
||||||
|
hideModal();
|
||||||
|
clearActiveDropdown();
|
||||||
|
}}
|
||||||
|
position={dropdownPosition}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</InnerNextStepDropdown>
|
||||||
|
</HandleContext.Provider>
|
||||||
|
)}
|
||||||
|
</AgentInstanceContext.Provider>
|
||||||
|
<NotebookPen
|
||||||
|
className={cn('hidden absolute size-6', { block: imgVisible })}
|
||||||
|
ref={ref}
|
||||||
|
></NotebookPen>
|
||||||
|
{formDrawerVisible && (
|
||||||
|
<AgentInstanceContext.Provider
|
||||||
|
value={{ addCanvasNode, showFormDrawer }}
|
||||||
|
>
|
||||||
|
<FormSheet
|
||||||
|
node={clickedNode}
|
||||||
|
visible={formDrawerVisible}
|
||||||
|
hideModal={hideFormDrawer}
|
||||||
|
chatVisible={chatVisible}
|
||||||
|
singleDebugDrawerVisible={singleDebugDrawerVisible}
|
||||||
|
hideSingleDebugDrawer={hideSingleDebugDrawer}
|
||||||
|
showSingleDebugDrawer={showSingleDebugDrawer}
|
||||||
|
></FormSheet>
|
||||||
|
</AgentInstanceContext.Provider>
|
||||||
|
)}
|
||||||
|
{runVisible && (
|
||||||
|
<RunSheet
|
||||||
|
hideModal={hideRunOrChatDrawer}
|
||||||
|
showModal={showChatModal}
|
||||||
|
></RunSheet>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AgentCanvas;
|
||||||
116
web/src/pages/data-flow/canvas/node/agent-node.tsx
Normal file
116
web/src/pages/data-flow/canvas/node/agent-node.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import LLMLabel from '@/components/llm-select/llm-label';
|
||||||
|
import { IAgentNode } from '@/interfaces/database/flow';
|
||||||
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AgentExceptionMethod, NodeHandleId } from '../../constant';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
import { isBottomSubAgent } from '../../utils';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import styles from './index.less';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
|
||||||
|
function InnerAgentNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IAgentNode>) {
|
||||||
|
const edges = useGraphStore((state) => state.edges);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isHeadAgent = useMemo(() => {
|
||||||
|
return !isBottomSubAgent(edges, id);
|
||||||
|
}, [edges, id]);
|
||||||
|
|
||||||
|
const exceptionMethod = useMemo(() => {
|
||||||
|
return get(data, 'form.exception_method');
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const isGotoMethod = useMemo(() => {
|
||||||
|
return exceptionMethod === AgentExceptionMethod.Goto;
|
||||||
|
}, [exceptionMethod]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolBar selected={selected} id={id} label={data.label}>
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
{isHeadAgent && (
|
||||||
|
<>
|
||||||
|
<CommonHandle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
></CommonHandle>
|
||||||
|
<CommonHandle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
id={NodeHandleId.Start}
|
||||||
|
isConnectableEnd={false}
|
||||||
|
></CommonHandle>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
isConnectable={false}
|
||||||
|
id={NodeHandleId.AgentTop}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
isConnectable={false}
|
||||||
|
id={NodeHandleId.AgentBottom}
|
||||||
|
style={{ left: 180 }}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
isConnectable={false}
|
||||||
|
id={NodeHandleId.Tool}
|
||||||
|
style={{ left: 20 }}
|
||||||
|
></Handle>
|
||||||
|
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<div className={'bg-bg-card rounded-sm p-1'}>
|
||||||
|
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||||
|
</div>
|
||||||
|
{(isGotoMethod ||
|
||||||
|
exceptionMethod === AgentExceptionMethod.Comment) && (
|
||||||
|
<div className="bg-bg-card rounded-sm p-1 flex justify-between gap-2">
|
||||||
|
<span className="text-text-secondary">{t('flow.onFailure')}</span>
|
||||||
|
<span className="truncate flex-1 text-right">
|
||||||
|
{t(`flow.${exceptionMethod}`)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
{isGotoMethod && (
|
||||||
|
<CommonHandle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className="!bg-state-error"
|
||||||
|
style={{ ...RightHandleStyle, top: 94 }}
|
||||||
|
nodeId={id}
|
||||||
|
id={NodeHandleId.AgentException}
|
||||||
|
isConnectableEnd={false}
|
||||||
|
></CommonHandle>
|
||||||
|
)}
|
||||||
|
</NodeWrapper>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgentNode = memo(InnerAgentNode);
|
||||||
62
web/src/pages/data-flow/canvas/node/begin-node.tsx
Normal file
62
web/src/pages/data-flow/canvas/node/begin-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 InnerBeginNode({ 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(`flow.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 BeginNode = memo(InnerBeginNode);
|
||||||
57
web/src/pages/data-flow/canvas/node/card.tsx
Normal file
57
web/src/pages/data-flow/canvas/node/card.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
|
||||||
|
export function CardWithForm() {
|
||||||
|
return (
|
||||||
|
<Card className="w-[350px]">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Create project</CardTitle>
|
||||||
|
<CardDescription>Deploy your new project in one-click.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form>
|
||||||
|
<div className="grid w-full items-center gap-4">
|
||||||
|
<div className="flex flex-col space-y-1.5">
|
||||||
|
<Label htmlFor="name">Name</Label>
|
||||||
|
<Input id="name" placeholder="Name of your project" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1.5">
|
||||||
|
<Label htmlFor="framework">Framework</Label>
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger id="framework">
|
||||||
|
<SelectValue placeholder="Select" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent position="popper">
|
||||||
|
<SelectItem value="next">Next.js</SelectItem>
|
||||||
|
<SelectItem value="sveltekit">SvelteKit</SelectItem>
|
||||||
|
<SelectItem value="astro">Astro</SelectItem>
|
||||||
|
<SelectItem value="nuxt">Nuxt.js</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-between">
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
<Button>Deploy</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
web/src/pages/data-flow/canvas/node/categorize-node.tsx
Normal file
62
web/src/pages/data-flow/canvas/node/categorize-node.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import LLMLabel from '@/components/llm-select/llm-label';
|
||||||
|
import { ICategorizeNode } from '@/interfaces/database/flow';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { NodeHandleId } from '../../constant';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { RightHandleStyle } from './handle-icon';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
import { useBuildCategorizeHandlePositions } from './use-build-categorize-handle-positions';
|
||||||
|
|
||||||
|
export function InnerCategorizeNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<ICategorizeNode>) {
|
||||||
|
const { positions } = useBuildCategorizeHandlePositions({ data, id });
|
||||||
|
return (
|
||||||
|
<ToolBar selected={selected} id={id} label={data.label}>
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
|
||||||
|
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
<div className={'bg-bg-card rounded-sm px-1'}>
|
||||||
|
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||||
|
</div>
|
||||||
|
{positions.map((position) => {
|
||||||
|
return (
|
||||||
|
<div key={position.uuid}>
|
||||||
|
<div className={'bg-bg-card rounded-sm p-1 truncate'}>
|
||||||
|
{position.name}
|
||||||
|
</div>
|
||||||
|
<CommonHandle
|
||||||
|
// key={position.text}
|
||||||
|
id={position.uuid}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable
|
||||||
|
style={{ ...RightHandleStyle, top: position.top }}
|
||||||
|
nodeId={id}
|
||||||
|
isConnectableEnd={false}
|
||||||
|
></CommonHandle>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</NodeWrapper>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategorizeNode = memo(InnerCategorizeNode);
|
||||||
58
web/src/pages/data-flow/canvas/node/dropdown.tsx
Normal file
58
web/src/pages/data-flow/canvas/node/dropdown.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import OperateDropdown from '@/components/operate-dropdown';
|
||||||
|
import { CopyOutlined } from '@ant-design/icons';
|
||||||
|
import { Flex, MenuProps } from 'antd';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Operator } from '../../constant';
|
||||||
|
import { useDuplicateNode } from '../../hooks';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
id: string;
|
||||||
|
iconFontColor?: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
|
||||||
|
const deleteIterationNodeById = useGraphStore(
|
||||||
|
(store) => store.deleteIterationNodeById,
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteNode = useCallback(() => {
|
||||||
|
if (label === Operator.Iteration) {
|
||||||
|
deleteIterationNodeById(id);
|
||||||
|
} else {
|
||||||
|
deleteNodeById(id);
|
||||||
|
}
|
||||||
|
}, [label, deleteIterationNodeById, id, deleteNodeById]);
|
||||||
|
|
||||||
|
const duplicateNode = useDuplicateNode();
|
||||||
|
|
||||||
|
const items: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
onClick: () => duplicateNode(id, label),
|
||||||
|
label: (
|
||||||
|
<Flex justify={'space-between'}>
|
||||||
|
{t('common.copy')}
|
||||||
|
<CopyOutlined />
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OperateDropdown
|
||||||
|
iconFontSize={22}
|
||||||
|
height={14}
|
||||||
|
deleteItem={deleteNode}
|
||||||
|
items={items}
|
||||||
|
needsDeletionValidation={false}
|
||||||
|
iconFontColor={iconFontColor}
|
||||||
|
></OperateDropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NodeDropdown;
|
||||||
@ -0,0 +1,295 @@
|
|||||||
|
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 { t } from 'i18next';
|
||||||
|
import { lowerFirst } from 'lodash';
|
||||||
|
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) => {
|
||||||
|
const contextData = handleContext || {
|
||||||
|
nodeId: '',
|
||||||
|
id: '',
|
||||||
|
type: 'source' as const,
|
||||||
|
position: Position.Right,
|
||||||
|
isFromConnectionDrag: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockEvent = mousePosition
|
||||||
|
? {
|
||||||
|
clientX: mousePosition.x,
|
||||||
|
clientY: mousePosition.y,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InnerNextStepDropdown({
|
||||||
|
children,
|
||||||
|
hideModal,
|
||||||
|
position,
|
||||||
|
onNodeCreated,
|
||||||
|
}: PropsWithChildren &
|
||||||
|
IModalProps<any> & {
|
||||||
|
position?: { x: number; y: number };
|
||||||
|
onNodeCreated?: (newNodeId: string) => void;
|
||||||
|
}) {
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (position && hideModal) {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [position, hideModal]);
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: position.x,
|
||||||
|
top: position.y + 10,
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="w-[300px] font-semibold bg-bg-base border border-border rounded-md shadow-lg">
|
||||||
|
<div className="px-3 py-2 border-b border-border">
|
||||||
|
<div className="text-sm font-medium">{t('flow.nextStep')}</div>
|
||||||
|
</div>
|
||||||
|
<HideModalContext.Provider value={hideModal}>
|
||||||
|
<OnNodeCreatedContext.Provider value={onNodeCreated}>
|
||||||
|
<AccordionOperators
|
||||||
|
isCustomDropdown={true}
|
||||||
|
mousePosition={position}
|
||||||
|
></AccordionOperators>
|
||||||
|
</OnNodeCreatedContext.Provider>
|
||||||
|
</HideModalContext.Provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu
|
||||||
|
open={true}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open && hideModal) {
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="w-[300px] font-semibold"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel>
|
||||||
|
<HideModalContext.Provider value={hideModal}>
|
||||||
|
<AccordionOperators></AccordionOperators>
|
||||||
|
</HideModalContext.Provider>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NextStepDropdown = memo(InnerNextStepDropdown);
|
||||||
80
web/src/pages/data-flow/canvas/node/email-node.tsx
Normal file
80
web/src/pages/data-flow/canvas/node/email-node.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { IEmailNode } from '@/interfaces/database/flow';
|
||||||
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { Flex } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import styles from './index.less';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
|
||||||
|
export function InnerEmailNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IEmailNode>) {
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={classNames(styles.ragNode, {
|
||||||
|
[styles.selectedNode]: selected,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
id="c"
|
||||||
|
type="source"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
id="b"
|
||||||
|
></Handle>
|
||||||
|
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||||
|
|
||||||
|
<Flex vertical gap={8} className={styles.emailNodeContainer}>
|
||||||
|
<div
|
||||||
|
className={styles.emailConfig}
|
||||||
|
onClick={() => setShowDetails(!showDetails)}
|
||||||
|
>
|
||||||
|
<div className={styles.configItem}>
|
||||||
|
<span className={styles.configLabel}>SMTP:</span>
|
||||||
|
<span className={styles.configValue}>{data.form?.smtp_server}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.configItem}>
|
||||||
|
<span className={styles.configLabel}>Port:</span>
|
||||||
|
<span className={styles.configValue}>{data.form?.smtp_port}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.configItem}>
|
||||||
|
<span className={styles.configLabel}>From:</span>
|
||||||
|
<span className={styles.configValue}>{data.form?.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.expandIcon}>{showDetails ? '▼' : '▶'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showDetails && (
|
||||||
|
<div className={styles.jsonExample}>
|
||||||
|
<div className={styles.jsonTitle}>Expected Input JSON:</div>
|
||||||
|
<pre className={styles.jsonContent}>
|
||||||
|
{`{
|
||||||
|
"to_email": "...",
|
||||||
|
"cc_email": "...",
|
||||||
|
"subject": "...",
|
||||||
|
"content": "..."
|
||||||
|
}`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailNode = memo(InnerEmailNode);
|
||||||
60
web/src/pages/data-flow/canvas/node/generate-node.tsx
Normal file
60
web/src/pages/data-flow/canvas/node/generate-node.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import LLMLabel from '@/components/llm-select/llm-label';
|
||||||
|
import { useTheme } from '@/components/theme-provider';
|
||||||
|
import { IGenerateNode } from '@/interfaces/database/flow';
|
||||||
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import styles from './index.less';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
|
||||||
|
export function InnerGenerateNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IGenerateNode>) {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={classNames(
|
||||||
|
styles.logicNode,
|
||||||
|
theme === 'dark' ? styles.dark : '',
|
||||||
|
{
|
||||||
|
[styles.selectedNode]: selected,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
id="c"
|
||||||
|
type="source"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
id="b"
|
||||||
|
></Handle>
|
||||||
|
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={data.name}
|
||||||
|
label={data.label}
|
||||||
|
className={styles.nodeHeader}
|
||||||
|
></NodeHeader>
|
||||||
|
|
||||||
|
<div className={styles.nodeText}>
|
||||||
|
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GenerateNode = memo(InnerGenerateNode);
|
||||||
20
web/src/pages/data-flow/canvas/node/handle-icon.tsx
Normal file
20
web/src/pages/data-flow/canvas/node/handle-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export const HandleIcon = () => {
|
||||||
|
return (
|
||||||
|
<PlusOutlined
|
||||||
|
style={{ fontSize: 6, color: 'white', position: 'absolute', zIndex: 10 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RightHandleStyle: CSSProperties = {
|
||||||
|
right: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeftHandleStyle: CSSProperties = {
|
||||||
|
left: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HandleIcon;
|
||||||
64
web/src/pages/data-flow/canvas/node/handle.tsx
Normal file
64
web/src/pages/data-flow/canvas/node/handle.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Handle, HandleProps } from '@xyflow/react';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { HandleContext } from '../../context';
|
||||||
|
import { useDropdownManager } from '../context';
|
||||||
|
import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
|
||||||
|
|
||||||
|
export function CommonHandle({
|
||||||
|
className,
|
||||||
|
nodeId,
|
||||||
|
...props
|
||||||
|
}: HandleProps & { nodeId: string }) {
|
||||||
|
const { visible, hideModal, showModal } = useSetModalState();
|
||||||
|
|
||||||
|
const { canShowDropdown, setActiveDropdown, clearActiveDropdown } =
|
||||||
|
useDropdownManager();
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
nodeId,
|
||||||
|
id: props.id || undefined,
|
||||||
|
type: props.type,
|
||||||
|
position: props.position,
|
||||||
|
isFromConnectionDrag: false,
|
||||||
|
}),
|
||||||
|
[nodeId, props.id, props.position, props.type],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HandleContext.Provider value={value}>
|
||||||
|
<Handle
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex justify-center items-center !bg-accent-primary !size-4 !rounded-sm !border-none ',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (!canShowDropdown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveDropdown('handle');
|
||||||
|
showModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-3 pointer-events-none text-text-title-invert" />
|
||||||
|
{visible && (
|
||||||
|
<InnerNextStepDropdown
|
||||||
|
hideModal={() => {
|
||||||
|
hideModal();
|
||||||
|
clearActiveDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</InnerNextStepDropdown>
|
||||||
|
)}
|
||||||
|
</Handle>
|
||||||
|
</HandleContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
285
web/src/pages/data-flow/canvas/node/index.less
Normal file
285
web/src/pages/data-flow/canvas/node/index.less
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
.dark {
|
||||||
|
background: rgb(63, 63, 63) !important;
|
||||||
|
}
|
||||||
|
.ragNode {
|
||||||
|
.commonNode();
|
||||||
|
.nodeName {
|
||||||
|
font-size: 10px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
color: #777;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categorizeAnchorPointText {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
left: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@lightBackgroundColor: rgba(150, 150, 150, 0.1);
|
||||||
|
@darkBackgroundColor: rgba(150, 150, 150, 0.2);
|
||||||
|
|
||||||
|
.selectedNode {
|
||||||
|
border: 1.5px solid rgb(59, 118, 244);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedIterationNode {
|
||||||
|
border-bottom: 1.5px solid rgb(59, 118, 244);
|
||||||
|
border-left: 1.5px solid rgb(59, 118, 244);
|
||||||
|
border-right: 1.5px solid rgb(59, 118, 244);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iterationHeader {
|
||||||
|
.commonNodeShadow();
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedHeader {
|
||||||
|
border-top: 1.9px solid rgb(59, 118, 244);
|
||||||
|
border-left: 1.9px solid rgb(59, 118, 244);
|
||||||
|
border-right: 1.9px solid rgb(59, 118, 244);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: rgb(59, 88, 253);
|
||||||
|
border: 1px solid white;
|
||||||
|
z-index: 1;
|
||||||
|
background-image: url('@/assets/svg/plus.svg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsonView {
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: auto;
|
||||||
|
max-width: 300px;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logicNode {
|
||||||
|
.commonNode();
|
||||||
|
|
||||||
|
.nodeName {
|
||||||
|
font-size: 10px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
color: #777;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categorizeAnchorPointText {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
left: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.relevantSourceLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.noteNode {
|
||||||
|
.commonNode();
|
||||||
|
min-width: 140px;
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
min-height: 128px;
|
||||||
|
.noteTitle {
|
||||||
|
background-color: #edfcff;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 6px 4px;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
.noteTitleDark {
|
||||||
|
background-color: #edfcff;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 6px 4px;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
.noteForm {
|
||||||
|
margin-top: 4px;
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
.noteName {
|
||||||
|
padding: 0px 4px;
|
||||||
|
}
|
||||||
|
.noteTextarea {
|
||||||
|
resize: none;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
height: 100%;
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.iterationNode {
|
||||||
|
.commonNodeShadow();
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodeText {
|
||||||
|
padding-inline: 0.4em;
|
||||||
|
padding-block: 0.2em 0.1em;
|
||||||
|
background: @lightBackgroundColor;
|
||||||
|
border-radius: 3px;
|
||||||
|
min-height: 22px;
|
||||||
|
.textEllipsis();
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodeHeader {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zeroDivider {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditionBlock {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
background: @lightBackgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditionLine {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: @darkBackgroundColor;
|
||||||
|
.textEllipsis();
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditionKey {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditionOperator {
|
||||||
|
padding: 0 2px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relevantLabel {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledgeNodeName {
|
||||||
|
.textEllipsis();
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageNodeContainer {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generateParameters {
|
||||||
|
padding-top: 8px;
|
||||||
|
label {
|
||||||
|
flex: 2;
|
||||||
|
.textEllipsis();
|
||||||
|
}
|
||||||
|
.parameterValue {
|
||||||
|
flex: 3;
|
||||||
|
.conditionLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailNodeContainer {
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.emailConfig {
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.configItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.configLabel {
|
||||||
|
color: #666;
|
||||||
|
width: 45px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.configValue {
|
||||||
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandIcon {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsonExample {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
animation: slideDown 0.2s ease-out;
|
||||||
|
|
||||||
|
.jsonTitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsonContent {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
web/src/pages/data-flow/canvas/node/index.tsx
Normal file
49
web/src/pages/data-flow/canvas/node/index.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { IRagNode } from '@/interfaces/database/flow';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { NodeHandleId } from '../../constant';
|
||||||
|
import { needsSingleStepDebugging } from '../../utils';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
|
||||||
|
function InnerRagNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IRagNode>) {
|
||||||
|
return (
|
||||||
|
<ToolBar
|
||||||
|
selected={selected}
|
||||||
|
id={id}
|
||||||
|
label={data.label}
|
||||||
|
showRun={needsSingleStepDebugging(data.label)}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</NodeWrapper>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RagNode = memo(InnerRagNode);
|
||||||
62
web/src/pages/data-flow/canvas/node/invoke-node.tsx
Normal file
62
web/src/pages/data-flow/canvas/node/invoke-node.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useTheme } from '@/components/theme-provider';
|
||||||
|
import { IInvokeNode } from '@/interfaces/database/flow';
|
||||||
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { Flex } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import styles from './index.less';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
|
||||||
|
function InnerInvokeNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IInvokeNode>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const url = get(data, 'form.url');
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={classNames(
|
||||||
|
styles.ragNode,
|
||||||
|
theme === 'dark' ? styles.dark : '',
|
||||||
|
{
|
||||||
|
[styles.selectedNode]: selected,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
id="c"
|
||||||
|
type="source"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
id="b"
|
||||||
|
style={RightHandleStyle}
|
||||||
|
></Handle>
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={data.name}
|
||||||
|
label={data.label}
|
||||||
|
className={styles.nodeHeader}
|
||||||
|
></NodeHeader>
|
||||||
|
<Flex vertical>
|
||||||
|
<div>{t('flow.url')}</div>
|
||||||
|
<div className={styles.nodeText}>{url}</div>
|
||||||
|
</Flex>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvokeNode = memo(InnerInvokeNode);
|
||||||
93
web/src/pages/data-flow/canvas/node/iteration-node.tsx
Normal file
93
web/src/pages/data-flow/canvas/node/iteration-node.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
IIterationNode,
|
||||||
|
IIterationStartNode,
|
||||||
|
} from '@/interfaces/database/flow';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { NodeProps, NodeResizeControl, Position } from '@xyflow/react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
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 NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ResizeIcon, controlStyle } from './resize-icon';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
|
||||||
|
export function InnerIterationNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IIterationNode>) {
|
||||||
|
return (
|
||||||
|
<ToolBar selected={selected} id={id} label={data.label} showRun={false}>
|
||||||
|
<section
|
||||||
|
className={cn('h-full bg-transparent rounded-b-md ', {
|
||||||
|
[styles.selectedHeader]: selected,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}>
|
||||||
|
<ResizeIcon />
|
||||||
|
</NodeResizeControl>
|
||||||
|
<CommonHandle
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
<CommonHandle
|
||||||
|
id={NodeHandleId.Start}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={data.name}
|
||||||
|
label={data.label}
|
||||||
|
wrapperClassName={cn(
|
||||||
|
'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-44px] left-[-0.3px]',
|
||||||
|
{
|
||||||
|
[styles.selectedHeader]: selected,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
></NodeHeader>
|
||||||
|
</section>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InnerIterationStartNode({
|
||||||
|
isConnectable = true,
|
||||||
|
id,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IIterationStartNode>) {
|
||||||
|
return (
|
||||||
|
<NodeWrapper className="w-20" selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
isConnectableEnd={false}
|
||||||
|
id={NodeHandleId.Start}
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
<div>
|
||||||
|
<OperatorIcon name={Operator.Begin}></OperatorIcon>
|
||||||
|
</div>
|
||||||
|
</NodeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IterationStartNode = memo(InnerIterationStartNode);
|
||||||
|
|
||||||
|
export const IterationNode = memo(InnerIterationNode);
|
||||||
60
web/src/pages/data-flow/canvas/node/keyword-node.tsx
Normal file
60
web/src/pages/data-flow/canvas/node/keyword-node.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import LLMLabel from '@/components/llm-select/llm-label';
|
||||||
|
import { useTheme } from '@/components/theme-provider';
|
||||||
|
import { IKeywordNode } from '@/interfaces/database/flow';
|
||||||
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import styles from './index.less';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
|
||||||
|
export function InnerKeywordNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IKeywordNode>) {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={classNames(
|
||||||
|
styles.logicNode,
|
||||||
|
theme === 'dark' ? styles.dark : '',
|
||||||
|
{
|
||||||
|
[styles.selectedNode]: selected,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
id="c"
|
||||||
|
type="source"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
id="b"
|
||||||
|
></Handle>
|
||||||
|
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={data.name}
|
||||||
|
label={data.label}
|
||||||
|
className={styles.nodeHeader}
|
||||||
|
></NodeHeader>
|
||||||
|
|
||||||
|
<div className={styles.nodeText}>
|
||||||
|
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KeywordNode = memo(InnerKeywordNode);
|
||||||
41
web/src/pages/data-flow/canvas/node/logic-node.tsx
Normal file
41
web/src/pages/data-flow/canvas/node/logic-node.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ILogicNode } from '@/interfaces/database/flow';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
|
||||||
|
export function InnerLogicNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<ILogicNode>) {
|
||||||
|
return (
|
||||||
|
<ToolBar selected={selected} id={id} label={data.label}>
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
id="c"
|
||||||
|
type="source"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
<CommonHandle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
id="b"
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||||
|
</NodeWrapper>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogicNode = memo(InnerLogicNode);
|
||||||
65
web/src/pages/data-flow/canvas/node/message-node.tsx
Normal file
65
web/src/pages/data-flow/canvas/node/message-node.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { IMessageNode } from '@/interfaces/database/flow';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { Flex } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { NodeHandleId } from '../../constant';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { LeftHandleStyle } from './handle-icon';
|
||||||
|
import styles from './index.less';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
|
||||||
|
function InnerMessageNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IMessageNode>) {
|
||||||
|
const messages: string[] = get(data, 'form.messages', []);
|
||||||
|
return (
|
||||||
|
<ToolBar selected={selected} id={id} label={data.label}>
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
></CommonHandle>
|
||||||
|
{/* <CommonHandle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
id={NodeHandleId.Start}
|
||||||
|
nodeId={id}
|
||||||
|
isConnectableEnd={false}
|
||||||
|
></CommonHandle> */}
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={data.name}
|
||||||
|
label={data.label}
|
||||||
|
className={classNames({
|
||||||
|
[styles.nodeHeader]: messages.length > 0,
|
||||||
|
})}
|
||||||
|
></NodeHeader>
|
||||||
|
|
||||||
|
<Flex vertical gap={8} className={styles.messageNodeContainer}>
|
||||||
|
{messages.map((message, idx) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.nodeText} key={idx}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</NodeWrapper>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MessageNode = memo(InnerMessageNode);
|
||||||
34
web/src/pages/data-flow/canvas/node/node-header.tsx
Normal file
34
web/src/pages/data-flow/canvas/node/node-header.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { Operator } from '../../constant';
|
||||||
|
import OperatorIcon from '../../operator-icon';
|
||||||
|
interface IProps {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
gap?: number;
|
||||||
|
className?: string;
|
||||||
|
wrapperClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InnerNodeHeader = ({
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
wrapperClassName,
|
||||||
|
}: IProps) => {
|
||||||
|
return (
|
||||||
|
<section className={cn(wrapperClassName, 'pb-4')}>
|
||||||
|
<div className={cn(className, 'flex gap-2.5')}>
|
||||||
|
<OperatorIcon name={label as Operator}></OperatorIcon>
|
||||||
|
<span className="truncate text-center font-semibold text-sm">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeHeader = memo(InnerNodeHeader);
|
||||||
|
|
||||||
|
export default NodeHeader;
|
||||||
18
web/src/pages/data-flow/canvas/node/node-wrapper.tsx
Normal file
18
web/src/pages/data-flow/canvas/node/node-wrapper.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
type IProps = HTMLAttributes<HTMLDivElement> & { selected?: boolean };
|
||||||
|
|
||||||
|
export function NodeWrapper({ children, className, selected }: IProps) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cn(
|
||||||
|
'bg-text-title-invert p-2.5 rounded-sm w-[200px] text-xs',
|
||||||
|
{ 'border border-accent-primary': selected },
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
web/src/pages/data-flow/canvas/node/note-node/index.tsx
Normal file
104
web/src/pages/data-flow/canvas/node/note-node/index.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { NodeProps, NodeResizeControl } from '@xyflow/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { INoteNode } from '@/interfaces/database/flow';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { NotebookPen } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { NodeWrapper } from '../node-wrapper';
|
||||||
|
import { ResizeIcon, controlStyle } from '../resize-icon';
|
||||||
|
import { useWatchFormChange, useWatchNameFormChange } from './use-watch-change';
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
text: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const NameFormSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function NoteNode({ data, id, selected }: NodeProps<INoteNode>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
defaultValues: data.form,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nameForm = useForm<z.infer<typeof NameFormSchema>>({
|
||||||
|
resolver: zodResolver(NameFormSchema),
|
||||||
|
defaultValues: { name: data.name },
|
||||||
|
});
|
||||||
|
|
||||||
|
useWatchFormChange(id, form);
|
||||||
|
|
||||||
|
useWatchNameFormChange(id, nameForm);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeWrapper
|
||||||
|
className="p-0 w-full h-full flex flex-col"
|
||||||
|
selected={selected}
|
||||||
|
>
|
||||||
|
<NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}>
|
||||||
|
<ResizeIcon />
|
||||||
|
</NodeResizeControl>
|
||||||
|
<section className="p-2 flex gap-2 bg-background-note items-center note-drag-handle rounded-t">
|
||||||
|
<NotebookPen className="size-4" />
|
||||||
|
<Form {...nameForm}>
|
||||||
|
<form className="flex-1">
|
||||||
|
<FormField
|
||||||
|
control={nameForm.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="h-full">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={t('flow.notePlaceholder')}
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
className="bg-transparent border-none focus-visible:outline focus-visible:outline-text-sub-title"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</section>
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="flex-1 p-1">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="text"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="h-full">
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t('flow.notePlaceholder')}
|
||||||
|
className="resize-none rounded-none p-1 h-full overflow-auto bg-transparent focus-visible:ring-0 border-none"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</NodeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(NoteNode);
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import useGraphStore from '@/pages/agent/store';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
|
||||||
|
let values = useWatch({ control: form?.control });
|
||||||
|
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Manually triggered form updates are synchronized to the canvas
|
||||||
|
if (id) {
|
||||||
|
values = form?.getValues() || {};
|
||||||
|
let nextValues: any = values;
|
||||||
|
|
||||||
|
updateNodeForm(id, nextValues);
|
||||||
|
}
|
||||||
|
}, [id, updateNodeForm, values]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWatchNameFormChange(id?: string, form?: UseFormReturn<any>) {
|
||||||
|
let values = useWatch({ control: form?.control });
|
||||||
|
const updateNodeName = useGraphStore((state) => state.updateNodeName);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Manually triggered form updates are synchronized to the canvas
|
||||||
|
if (id) {
|
||||||
|
updateNodeName(id, values.name);
|
||||||
|
}
|
||||||
|
}, [id, updateNodeName, values]);
|
||||||
|
}
|
||||||
121
web/src/pages/data-flow/canvas/node/popover.tsx
Normal file
121
web/src/pages/data-flow/canvas/node/popover.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import get from 'lodash/get';
|
||||||
|
import React, { MouseEventHandler, useCallback, useMemo } from 'react';
|
||||||
|
import JsonView from 'react18-json-view';
|
||||||
|
import 'react18-json-view/src/style.css';
|
||||||
|
import { useReplaceIdWithText } from '../../hooks';
|
||||||
|
|
||||||
|
import { useTheme } from '@/components/theme-provider';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||||
|
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
|
||||||
|
|
||||||
|
interface IProps extends React.PropsWithChildren {
|
||||||
|
nodeId: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NextNodePopover({ children, nodeId, name }: IProps) {
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
|
||||||
|
const { data } = useFetchAgent();
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const component = useMemo(() => {
|
||||||
|
return get(data, ['dsl', 'components', nodeId], {});
|
||||||
|
}, [nodeId, data]);
|
||||||
|
|
||||||
|
const inputs: Array<{ component_id: string; content: string }> = get(
|
||||||
|
component,
|
||||||
|
['obj', 'inputs'],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const output = get(component, ['obj', 'output'], {});
|
||||||
|
const { replacedOutput } = useReplaceIdWithText(output);
|
||||||
|
const stopPropagation: MouseEventHandler = useCallback((e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getLabel = useGetComponentLabelByValue(nodeId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger onClick={stopPropagation} asChild>
|
||||||
|
{children}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
align={'start'}
|
||||||
|
side={'right'}
|
||||||
|
sideOffset={20}
|
||||||
|
onClick={stopPropagation}
|
||||||
|
className="w-[400px]"
|
||||||
|
>
|
||||||
|
<div className="mb-3 font-semibold text-[16px]">
|
||||||
|
{name} {t('operationResults')}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full gap-4 flex-col">
|
||||||
|
<div className="flex flex-col space-y-1.5">
|
||||||
|
<span className="font-semibold text-[14px]">{t('input')}</span>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
theme === 'dark'
|
||||||
|
? {
|
||||||
|
backgroundColor: 'rgba(150, 150, 150, 0.2)',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
className={`bg-gray-100 p-1 rounded`}
|
||||||
|
>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>{t('componentId')}</TableHead>
|
||||||
|
<TableHead className="w-[60px]">{t('content')}</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{inputs.map((x, idx) => (
|
||||||
|
<TableRow key={idx}>
|
||||||
|
<TableCell>{getLabel(x.component_id)}</TableCell>
|
||||||
|
<TableCell className="truncate">{x.content}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1.5">
|
||||||
|
<span className="font-semibold text-[14px]">{t('output')}</span>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
theme === 'dark'
|
||||||
|
? {
|
||||||
|
backgroundColor: 'rgba(150, 150, 150, 0.2)',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
className="bg-gray-100 p-1 rounded"
|
||||||
|
>
|
||||||
|
<JsonView
|
||||||
|
src={replacedOutput}
|
||||||
|
displaySize={30}
|
||||||
|
className="w-full max-h-[300px] break-words overflow-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
web/src/pages/data-flow/canvas/node/relevant-node.tsx
Normal file
73
web/src/pages/data-flow/canvas/node/relevant-node.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { Flex } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { RightHandleStyle } from './handle-icon';
|
||||||
|
|
||||||
|
import { useTheme } from '@/components/theme-provider';
|
||||||
|
import { IRelevantNode } from '@/interfaces/database/flow';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useReplaceIdWithName } from '../../hooks';
|
||||||
|
import styles from './index.less';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
|
||||||
|
function InnerRelevantNode({ id, data, selected }: NodeProps<IRelevantNode>) {
|
||||||
|
const yes = get(data, 'form.yes');
|
||||||
|
const no = get(data, 'form.no');
|
||||||
|
const replaceIdWithName = useReplaceIdWithName();
|
||||||
|
const { theme } = useTheme();
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={classNames(
|
||||||
|
styles.logicNode,
|
||||||
|
theme === 'dark' ? styles.dark : '',
|
||||||
|
{
|
||||||
|
[styles.selectedNode]: selected,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable
|
||||||
|
className={styles.handle}
|
||||||
|
id={'a'}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable
|
||||||
|
className={styles.handle}
|
||||||
|
id={'yes'}
|
||||||
|
style={{ ...RightHandleStyle, top: 57 + 20 }}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable
|
||||||
|
className={styles.handle}
|
||||||
|
id={'no'}
|
||||||
|
style={{ ...RightHandleStyle, top: 115 + 20 }}
|
||||||
|
></Handle>
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={data.name}
|
||||||
|
label={data.label}
|
||||||
|
className={styles.nodeHeader}
|
||||||
|
></NodeHeader>
|
||||||
|
|
||||||
|
<Flex vertical gap={10}>
|
||||||
|
<Flex vertical>
|
||||||
|
<div className={styles.relevantLabel}>Yes</div>
|
||||||
|
<div className={styles.nodeText}>{replaceIdWithName(yes)}</div>
|
||||||
|
</Flex>
|
||||||
|
<Flex vertical>
|
||||||
|
<div className={styles.relevantLabel}>No</div>
|
||||||
|
<div className={styles.nodeText}>{replaceIdWithName(no)}</div>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RelevantNode = memo(InnerRelevantNode);
|
||||||
32
web/src/pages/data-flow/canvas/node/resize-icon.tsx
Normal file
32
web/src/pages/data-flow/canvas/node/resize-icon.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export function ResizeIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="rgba(76, 164, 231, 1)"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 5,
|
||||||
|
bottom: 5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<polyline points="16 20 20 20 20 16" />
|
||||||
|
<line x1="14" y1="14" x2="20" y2="20" />
|
||||||
|
<polyline points="8 4 4 4 4 8" />
|
||||||
|
<line x1="4" y1="4" x2="10" y2="10" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const controlStyle = {
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'nwse-resize',
|
||||||
|
};
|
||||||
84
web/src/pages/data-flow/canvas/node/retrieval-node.tsx
Normal file
84
web/src/pages/data-flow/canvas/node/retrieval-node.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||||
|
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
||||||
|
import { IRetrievalNode } from '@/interfaces/database/flow';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { NodeHandleId } from '../../constant';
|
||||||
|
import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import styles from './index.less';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
|
||||||
|
function InnerRetrievalNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IRetrievalNode>) {
|
||||||
|
const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []);
|
||||||
|
const { list: knowledgeList } = useFetchKnowledgeList(true);
|
||||||
|
|
||||||
|
const getLabel = useGetVariableLabelByValue(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolBar selected={selected} id={id} label={data.label}>
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
<CommonHandle
|
||||||
|
id={NodeHandleId.Start}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
isConnectableEnd={false}
|
||||||
|
></CommonHandle>
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={data.name}
|
||||||
|
label={data.label}
|
||||||
|
className={classNames({
|
||||||
|
[styles.nodeHeader]: knowledgeBaseIds.length > 0,
|
||||||
|
})}
|
||||||
|
></NodeHeader>
|
||||||
|
<section className="flex flex-col gap-2">
|
||||||
|
{knowledgeBaseIds.map((id) => {
|
||||||
|
const item = knowledgeList.find((y) => id === y.id);
|
||||||
|
const label = getLabel(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.nodeText} key={id}>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<RAGFlowAvatar
|
||||||
|
className="size-6 rounded-lg"
|
||||||
|
avatar={id}
|
||||||
|
name={item?.name || (label as string) || 'CN'}
|
||||||
|
isPerson={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={'truncate flex-1'}>{label || item?.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</NodeWrapper>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RetrievalNode = memo(InnerRetrievalNode);
|
||||||
60
web/src/pages/data-flow/canvas/node/rewrite-node.tsx
Normal file
60
web/src/pages/data-flow/canvas/node/rewrite-node.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import LLMLabel from '@/components/llm-select/llm-label';
|
||||||
|
import { useTheme } from '@/components/theme-provider';
|
||||||
|
import { IRewriteNode } from '@/interfaces/database/flow';
|
||||||
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import styles from './index.less';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
|
||||||
|
function InnerRewriteNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IRewriteNode>) {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={classNames(
|
||||||
|
styles.logicNode,
|
||||||
|
theme === 'dark' ? styles.dark : '',
|
||||||
|
{
|
||||||
|
[styles.selectedNode]: selected,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
id="c"
|
||||||
|
type="source"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
id="b"
|
||||||
|
></Handle>
|
||||||
|
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={data.name}
|
||||||
|
label={data.label}
|
||||||
|
className={styles.nodeHeader}
|
||||||
|
></NodeHeader>
|
||||||
|
|
||||||
|
<div className={styles.nodeText}>
|
||||||
|
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RewriteNode = memo(InnerRewriteNode);
|
||||||
118
web/src/pages/data-flow/canvas/node/switch-node.tsx
Normal file
118
web/src/pages/data-flow/canvas/node/switch-node.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { NodeHandleId, SwitchOperatorOptions } from '../../constant';
|
||||||
|
import { LogicalOperatorIcon } from '../../form/switch-form';
|
||||||
|
import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { RightHandleStyle } from './handle-icon';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
import { useBuildSwitchHandlePositions } from './use-build-switch-handle-positions';
|
||||||
|
|
||||||
|
const getConditionKey = (idx: number, length: number) => {
|
||||||
|
if (idx === 0 && length !== 1) {
|
||||||
|
return 'If';
|
||||||
|
} else if (idx === length - 1) {
|
||||||
|
return 'Else';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'ElseIf';
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConditionBlock = ({
|
||||||
|
condition,
|
||||||
|
nodeId,
|
||||||
|
}: { condition: ISwitchCondition } & { nodeId: string }) => {
|
||||||
|
const items = condition?.items ?? [];
|
||||||
|
const getLabel = useGetVariableLabelByValue(nodeId);
|
||||||
|
|
||||||
|
const renderOperatorIcon = useCallback((operator?: string) => {
|
||||||
|
const item = SwitchOperatorOptions.find((x) => x.value === operator);
|
||||||
|
if (item) {
|
||||||
|
return (
|
||||||
|
<LogicalOperatorIcon
|
||||||
|
icon={item?.icon}
|
||||||
|
value={item?.value}
|
||||||
|
></LogicalOperatorIcon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0 divide-y divide-background-card">
|
||||||
|
{items.map((x, idx) => (
|
||||||
|
<div key={idx}>
|
||||||
|
<section className="flex justify-between gap-2 items-center text-xs p-1">
|
||||||
|
<div className="flex-1 truncate text-accent-primary">
|
||||||
|
{getLabel(x?.cpn_id)}
|
||||||
|
</div>
|
||||||
|
<span>{renderOperatorIcon(x?.operator)}</span>
|
||||||
|
<div className="flex-1 truncate">{x?.value}</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) {
|
||||||
|
const { positions } = useBuildSwitchHandlePositions({ data, id });
|
||||||
|
return (
|
||||||
|
<ToolBar selected={selected} id={id} label={data.label} showRun={false}>
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable
|
||||||
|
nodeId={id}
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
></CommonHandle>
|
||||||
|
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||||
|
<section className="gap-2.5 flex flex-col">
|
||||||
|
{positions.map((position, idx) => {
|
||||||
|
return (
|
||||||
|
<div key={idx}>
|
||||||
|
<section className="flex flex-col text-xs">
|
||||||
|
<div className="text-right">
|
||||||
|
<span>{getConditionKey(idx, positions.length)}</span>
|
||||||
|
<div className="text-text-secondary">
|
||||||
|
{idx < positions.length - 1 && position.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-accent-primary">
|
||||||
|
{idx < positions.length - 1 &&
|
||||||
|
position.condition?.logical_operator?.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
{position.condition && (
|
||||||
|
<ConditionBlock
|
||||||
|
condition={position.condition}
|
||||||
|
nodeId={id}
|
||||||
|
></ConditionBlock>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<CommonHandle
|
||||||
|
key={position.text}
|
||||||
|
id={position.text}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable
|
||||||
|
style={{ ...RightHandleStyle, top: position.top }}
|
||||||
|
nodeId={id}
|
||||||
|
isConnectableEnd={false}
|
||||||
|
></CommonHandle>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</NodeWrapper>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SwitchNode = memo(InnerSwitchNode);
|
||||||
78
web/src/pages/data-flow/canvas/node/template-node.tsx
Normal file
78
web/src/pages/data-flow/canvas/node/template-node.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useTheme } from '@/components/theme-provider';
|
||||||
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { Flex } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
|
||||||
|
import { IGenerateParameter } from '../../interface';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
|
||||||
|
import { ITemplateNode } from '@/interfaces/database/flow';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
function InnerTemplateNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<ITemplateNode>) {
|
||||||
|
const parameters: IGenerateParameter[] = get(data, 'form.parameters', []);
|
||||||
|
const getLabel = useGetComponentLabelByValue(id);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={classNames(
|
||||||
|
styles.logicNode,
|
||||||
|
theme === 'dark' ? styles.dark : '',
|
||||||
|
|
||||||
|
{
|
||||||
|
[styles.selectedNode]: selected,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
id="c"
|
||||||
|
type="source"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
></Handle>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className={styles.handle}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
id="b"
|
||||||
|
></Handle>
|
||||||
|
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={data.name}
|
||||||
|
label={data.label}
|
||||||
|
className={styles.nodeHeader}
|
||||||
|
></NodeHeader>
|
||||||
|
|
||||||
|
<Flex gap={8} vertical className={styles.generateParameters}>
|
||||||
|
{parameters.map((x) => (
|
||||||
|
<Flex
|
||||||
|
key={x.id}
|
||||||
|
align="center"
|
||||||
|
gap={6}
|
||||||
|
className={styles.conditionBlock}
|
||||||
|
>
|
||||||
|
<label htmlFor="">{x.key}</label>
|
||||||
|
<span className={styles.parameterValue}>
|
||||||
|
{getLabel(x.component_id)}
|
||||||
|
</span>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateNode = memo(InnerTemplateNode);
|
||||||
83
web/src/pages/data-flow/canvas/node/tool-node.tsx
Normal file
83
web/src/pages/data-flow/canvas/node/tool-node.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { IAgentForm, IToolNode } from '@/interfaces/database/agent';
|
||||||
|
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { MouseEventHandler, memo, useCallback } from 'react';
|
||||||
|
import { NodeHandleId, Operator } from '../../constant';
|
||||||
|
import { ToolCard } from '../../form/agent-form/agent-tools';
|
||||||
|
import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
|
||||||
|
import OperatorIcon from '../../operator-icon';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
|
||||||
|
function InnerToolNode({
|
||||||
|
id,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<IToolNode>) {
|
||||||
|
const { edges, getNode } = useGraphStore((state) => state);
|
||||||
|
const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source;
|
||||||
|
const upstreamAgentNode = getNode(upstreamAgentNodeId);
|
||||||
|
const { findMcpById } = useFindMcpById();
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(operator: string): MouseEventHandler<HTMLLIElement> =>
|
||||||
|
(e) => {
|
||||||
|
if (operator === Operator.Code) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const tools: IAgentForm['tools'] = get(
|
||||||
|
upstreamAgentNode,
|
||||||
|
'data.form.tools',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const mcpList: IAgentForm['mcp'] = get(
|
||||||
|
upstreamAgentNode,
|
||||||
|
'data.form.mcp',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<Handle
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
></Handle>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{tools.map((x) => (
|
||||||
|
<ToolCard
|
||||||
|
key={x.component_name}
|
||||||
|
onClick={handleClick(x.component_name)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
data-tool={x.component_name}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1 items-center pointer-events-none">
|
||||||
|
<OperatorIcon name={x.component_name as Operator}></OperatorIcon>
|
||||||
|
{x.component_name}
|
||||||
|
</div>
|
||||||
|
</ToolCard>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{mcpList.map((x) => (
|
||||||
|
<ToolCard
|
||||||
|
key={x.mcp_id}
|
||||||
|
onClick={handleClick(x.mcp_id)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
data-tool={x.mcp_id}
|
||||||
|
>
|
||||||
|
{findMcpById(x.mcp_id)?.name}
|
||||||
|
</ToolCard>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</NodeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolNode = memo(InnerToolNode);
|
||||||
88
web/src/pages/data-flow/canvas/node/toolbar.tsx
Normal file
88
web/src/pages/data-flow/canvas/node/toolbar.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
TooltipContent,
|
||||||
|
TooltipNode,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/xyflow/tooltip-node';
|
||||||
|
import { Position } from '@xyflow/react';
|
||||||
|
import { Copy, Play, Trash2 } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
HTMLAttributes,
|
||||||
|
MouseEventHandler,
|
||||||
|
PropsWithChildren,
|
||||||
|
useCallback,
|
||||||
|
} from 'react';
|
||||||
|
import { Operator } from '../../constant';
|
||||||
|
import { useDuplicateNode } from '../../hooks';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
|
||||||
|
function IconWrapper({ children, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div className="p-1.5 bg-text-title rounded-sm cursor-pointer" {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolBarProps = {
|
||||||
|
selected?: boolean | undefined;
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
showRun?: boolean;
|
||||||
|
} & PropsWithChildren;
|
||||||
|
|
||||||
|
export function ToolBar({
|
||||||
|
selected,
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
id,
|
||||||
|
showRun = true,
|
||||||
|
}: ToolBarProps) {
|
||||||
|
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
|
||||||
|
const deleteIterationNodeById = useGraphStore(
|
||||||
|
(store) => store.deleteIterationNodeById,
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteNode: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (label === Operator.Iteration) {
|
||||||
|
deleteIterationNodeById(id);
|
||||||
|
} else {
|
||||||
|
deleteNodeById(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[deleteIterationNodeById, deleteNodeById, id, label],
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicateNode = useDuplicateNode();
|
||||||
|
|
||||||
|
const handleDuplicate: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
duplicateNode(id, label);
|
||||||
|
},
|
||||||
|
[duplicateNode, id, label],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipNode selected={selected}>
|
||||||
|
<TooltipTrigger>{children}</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent position={Position.Top}>
|
||||||
|
<section className="flex gap-2 items-center">
|
||||||
|
{showRun && (
|
||||||
|
<IconWrapper>
|
||||||
|
<Play className="size-3.5" data-play />
|
||||||
|
</IconWrapper>
|
||||||
|
)}{' '}
|
||||||
|
<IconWrapper onClick={handleDuplicate}>
|
||||||
|
<Copy className="size-3.5" />
|
||||||
|
</IconWrapper>
|
||||||
|
<IconWrapper onClick={deleteNode}>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
</IconWrapper>
|
||||||
|
</section>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { useUpdateNodeInternals } from '@xyflow/react';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { useCreateCategorizeFormSchema } from '../../form/categorize-form/use-form-schema';
|
||||||
|
|
||||||
|
export const useBuildCategorizeHandlePositions = ({
|
||||||
|
data,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
data: RAGFlowNodeType['data'];
|
||||||
|
}) => {
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
|
||||||
|
const FormSchema = useCreateCategorizeFormSchema();
|
||||||
|
|
||||||
|
type FormSchemaType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
const items: Required<FormSchemaType['items']> = useMemo(() => {
|
||||||
|
return get(data, `form.items`, []);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const positions = useMemo(() => {
|
||||||
|
const list: Array<{
|
||||||
|
top: number;
|
||||||
|
name: string;
|
||||||
|
uuid: string;
|
||||||
|
}> &
|
||||||
|
Required<FormSchemaType['items']> = [];
|
||||||
|
|
||||||
|
items.forEach((x, idx) => {
|
||||||
|
list.push({
|
||||||
|
...x,
|
||||||
|
top: idx === 0 ? 86 : list[idx - 1].top + 8 + 24,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateNodeInternals(id);
|
||||||
|
}, [id, updateNodeInternals, items]);
|
||||||
|
|
||||||
|
return { positions };
|
||||||
|
};
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { ISwitchCondition, RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { useUpdateNodeInternals } from '@xyflow/react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { SwitchElseTo } from '../../constant';
|
||||||
|
import { generateSwitchHandleText } from '../../utils';
|
||||||
|
|
||||||
|
export const useBuildSwitchHandlePositions = ({
|
||||||
|
data,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
data: RAGFlowNodeType['data'];
|
||||||
|
}) => {
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
|
||||||
|
const conditions: ISwitchCondition[] = useMemo(() => {
|
||||||
|
return get(data, 'form.conditions', []);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const positions = useMemo(() => {
|
||||||
|
const list: Array<{
|
||||||
|
text: string;
|
||||||
|
top: number;
|
||||||
|
idx: number;
|
||||||
|
condition?: ISwitchCondition;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
[...conditions, ''].forEach((x, idx) => {
|
||||||
|
let top = idx === 0 ? 53 : list[idx - 1].top + 10 + 14 + 16 + 16; // case number (Case 1) height + flex gap
|
||||||
|
if (idx >= 1) {
|
||||||
|
const previousItems = conditions[idx - 1]?.items ?? [];
|
||||||
|
if (previousItems.length > 0) {
|
||||||
|
// top += 12; // ConditionBlock padding
|
||||||
|
top += previousItems.length * 26; // condition variable height
|
||||||
|
// top += (previousItems.length - 1) * 25; // operator height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.push({
|
||||||
|
text:
|
||||||
|
idx < conditions.length
|
||||||
|
? generateSwitchHandleText(idx)
|
||||||
|
: SwitchElseTo,
|
||||||
|
idx,
|
||||||
|
top,
|
||||||
|
condition: typeof x === 'string' ? undefined : x,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}, [conditions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateNodeInternals(id);
|
||||||
|
}, [id, updateNodeInternals, conditions]);
|
||||||
|
|
||||||
|
return { positions };
|
||||||
|
};
|
||||||
13
web/src/pages/data-flow/components/background.tsx
Normal file
13
web/src/pages/data-flow/components/background.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useIsDarkTheme } from '@/components/theme-provider';
|
||||||
|
import { Background } from '@xyflow/react';
|
||||||
|
|
||||||
|
export function AgentBackground() {
|
||||||
|
const isDarkTheme = useIsDarkTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Background
|
||||||
|
color={isDarkTheme ? 'rgba(255,255,255,0.15)' : '#A8A9B3'}
|
||||||
|
bgColor={isDarkTheme ? 'rgba(11, 11, 12, 1)' : 'rgba(0, 0, 0, 0.05)'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
947
web/src/pages/data-flow/constant.tsx
Normal file
947
web/src/pages/data-flow/constant.tsx
Normal file
@ -0,0 +1,947 @@
|
|||||||
|
import {
|
||||||
|
initialKeywordsSimilarityWeightValue,
|
||||||
|
initialSimilarityThresholdValue,
|
||||||
|
} from '@/components/similarity-slider';
|
||||||
|
import {
|
||||||
|
AgentGlobals,
|
||||||
|
CodeTemplateStrMap,
|
||||||
|
ProgrammingLanguage,
|
||||||
|
} from '@/constants/agent';
|
||||||
|
|
||||||
|
export enum AgentDialogueMode {
|
||||||
|
Conversational = 'conversational',
|
||||||
|
Task = 'task',
|
||||||
|
}
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChatVariableEnabledField,
|
||||||
|
variableEnabledFieldMap,
|
||||||
|
} from '@/constants/chat';
|
||||||
|
import { ModelVariableType } from '@/constants/knowledge';
|
||||||
|
import i18n from '@/locales/config';
|
||||||
|
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
|
||||||
|
// DuckDuckGo's channel options
|
||||||
|
export enum Channel {
|
||||||
|
Text = 'text',
|
||||||
|
News = 'news',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PromptRole {
|
||||||
|
User = 'user',
|
||||||
|
Assistant = 'assistant',
|
||||||
|
}
|
||||||
|
|
||||||
|
import {
|
||||||
|
Circle,
|
||||||
|
CircleSlash2,
|
||||||
|
CloudUpload,
|
||||||
|
ListOrdered,
|
||||||
|
OptionIcon,
|
||||||
|
TextCursorInput,
|
||||||
|
ToggleLeft,
|
||||||
|
WrapText,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
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',
|
||||||
|
Concentrator = 'Concentrator',
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SwitchLogicOperatorOptions = ['and', 'or'];
|
||||||
|
|
||||||
|
export const CommonOperatorList = Object.values(Operator).filter(
|
||||||
|
(x) => x !== Operator.Note,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentOperatorList = [
|
||||||
|
Operator.Retrieval,
|
||||||
|
Operator.Categorize,
|
||||||
|
Operator.Message,
|
||||||
|
Operator.RewriteQuestion,
|
||||||
|
Operator.KeywordExtract,
|
||||||
|
Operator.Switch,
|
||||||
|
Operator.Concentrator,
|
||||||
|
Operator.Iteration,
|
||||||
|
Operator.WaitingDialogue,
|
||||||
|
Operator.Note,
|
||||||
|
Operator.Agent,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const componentMenuList = [
|
||||||
|
{
|
||||||
|
name: Operator.Retrieval,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Categorize,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Message,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: Operator.RewriteQuestion,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.KeywordExtract,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Switch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Concentrator,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Iteration,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.WaitingDialogue,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Agent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Note,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.DuckDuckGo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Baidu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Wikipedia,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.PubMed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.ArXiv,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Google,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Bing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.GoogleScholar,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.DeepL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.GitHub,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.BaiduFanyi,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.QWeather,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.ExeSQL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.WenCai,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.AkShare,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.YahooFinance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Jin10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.TuShare,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Crawler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Invoke,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.Email,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Operator.SearXNG,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
const initialQueryBaseValues = {
|
||||||
|
query: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialRetrievalValues = {
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
top_n: 8,
|
||||||
|
top_k: 1024,
|
||||||
|
kb_ids: [],
|
||||||
|
rerank_id: '',
|
||||||
|
empty_response: '',
|
||||||
|
...initialSimilarityThresholdValue,
|
||||||
|
...initialKeywordsSimilarityWeightValue,
|
||||||
|
use_kg: false,
|
||||||
|
cross_languages: [],
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialBeginValues = {
|
||||||
|
mode: AgentDialogueMode.Conversational,
|
||||||
|
prologue: `Hi! I'm your assistant. What can I do for you?`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const variableCheckBoxFieldMap = Object.keys(
|
||||||
|
variableEnabledFieldMap,
|
||||||
|
).reduce<Record<string, boolean>>((pre, cur) => {
|
||||||
|
pre[cur] = setInitialChatVariableEnabledFieldValue(
|
||||||
|
cur as ChatVariableEnabledField,
|
||||||
|
);
|
||||||
|
return pre;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const initialLlmBaseValues = {
|
||||||
|
...variableCheckBoxFieldMap,
|
||||||
|
temperature: 0.1,
|
||||||
|
top_p: 0.3,
|
||||||
|
frequency_penalty: 0.7,
|
||||||
|
presence_penalty: 0.4,
|
||||||
|
max_tokens: 256,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialGenerateValues = {
|
||||||
|
...initialLlmBaseValues,
|
||||||
|
prompt: i18n.t('flow.promptText'),
|
||||||
|
cite: true,
|
||||||
|
message_history_window_size: 12,
|
||||||
|
parameters: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialRewriteQuestionValues = {
|
||||||
|
...initialLlmBaseValues,
|
||||||
|
language: '',
|
||||||
|
message_history_window_size: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialRelevantValues = {
|
||||||
|
...initialLlmBaseValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialCategorizeValues = {
|
||||||
|
...initialLlmBaseValues,
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
parameter: ModelVariableType.Precise,
|
||||||
|
message_history_window_size: 1,
|
||||||
|
items: [],
|
||||||
|
outputs: {
|
||||||
|
category_name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialMessageValues = {
|
||||||
|
content: [''],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialKeywordExtractValues = {
|
||||||
|
...initialLlmBaseValues,
|
||||||
|
top_n: 3,
|
||||||
|
...initialQueryBaseValues,
|
||||||
|
};
|
||||||
|
export const initialDuckValues = {
|
||||||
|
top_n: 10,
|
||||||
|
channel: Channel.Text,
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
value: [],
|
||||||
|
type: 'Array<Object>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialSearXNGValues = {
|
||||||
|
top_n: '10',
|
||||||
|
searxng_url: '',
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
value: [],
|
||||||
|
type: 'Array<Object>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialBaiduValues = {
|
||||||
|
top_n: 10,
|
||||||
|
...initialQueryBaseValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialWikipediaValues = {
|
||||||
|
top_n: 10,
|
||||||
|
language: 'en',
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialPubMedValues = {
|
||||||
|
top_n: 12,
|
||||||
|
email: '',
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialArXivValues = {
|
||||||
|
top_n: 12,
|
||||||
|
sort_by: 'relevance',
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialGoogleValues = {
|
||||||
|
q: AgentGlobals.SysQuery,
|
||||||
|
start: 0,
|
||||||
|
num: 12,
|
||||||
|
api_key: '',
|
||||||
|
country: 'us',
|
||||||
|
language: 'en',
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
value: [],
|
||||||
|
type: 'Array<Object>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialBingValues = {
|
||||||
|
top_n: 10,
|
||||||
|
channel: 'Webpages',
|
||||||
|
api_key:
|
||||||
|
'YOUR_API_KEY (obtained from https://www.microsoft.com/en-us/bing/apis/bing-web-search-api)',
|
||||||
|
country: 'CH',
|
||||||
|
language: 'en',
|
||||||
|
query: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialGoogleScholarValues = {
|
||||||
|
top_n: 12,
|
||||||
|
sort_by: 'relevance',
|
||||||
|
patents: true,
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
year_low: undefined,
|
||||||
|
year_high: undefined,
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
value: [],
|
||||||
|
type: 'Array<Object>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialDeepLValues = {
|
||||||
|
top_n: 5,
|
||||||
|
auth_key: 'relevance',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialGithubValues = {
|
||||||
|
top_n: 5,
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
value: [],
|
||||||
|
type: 'Array<Object>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialBaiduFanyiValues = {
|
||||||
|
appid: 'xxx',
|
||||||
|
secret_key: 'xxx',
|
||||||
|
trans_type: 'translate',
|
||||||
|
...initialQueryBaseValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialQWeatherValues = {
|
||||||
|
web_apikey: 'xxx',
|
||||||
|
type: 'weather',
|
||||||
|
user_type: 'free',
|
||||||
|
time_period: 'now',
|
||||||
|
...initialQueryBaseValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialExeSqlValues = {
|
||||||
|
sql: '',
|
||||||
|
db_type: 'mysql',
|
||||||
|
database: '',
|
||||||
|
username: '',
|
||||||
|
host: '',
|
||||||
|
port: 3306,
|
||||||
|
password: '',
|
||||||
|
max_records: 1024,
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
value: [],
|
||||||
|
type: 'Array<Object>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialSwitchValues = {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
logical_operator: SwitchLogicOperatorOptions[0],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
operator: SwitchOperatorOptions[0].value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
to: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[SwitchElseTo]: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialWenCaiValues = {
|
||||||
|
top_n: 20,
|
||||||
|
query_type: 'stock',
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
outputs: {
|
||||||
|
report: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialAkShareValues = { top_n: 10, ...initialQueryBaseValues };
|
||||||
|
|
||||||
|
export const initialYahooFinanceValues = {
|
||||||
|
stock_code: '',
|
||||||
|
info: true,
|
||||||
|
history: false,
|
||||||
|
financials: false,
|
||||||
|
balance_sheet: false,
|
||||||
|
cash_flow_statement: false,
|
||||||
|
news: true,
|
||||||
|
outputs: {
|
||||||
|
report: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialJin10Values = {
|
||||||
|
type: 'flash',
|
||||||
|
secret_key: 'xxx',
|
||||||
|
flash_type: '1',
|
||||||
|
contain: '',
|
||||||
|
filter: '',
|
||||||
|
...initialQueryBaseValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialConcentratorValues = {};
|
||||||
|
|
||||||
|
export const initialTuShareValues = {
|
||||||
|
token: 'xxx',
|
||||||
|
src: 'eastmoney',
|
||||||
|
start_date: '2024-01-01 09:00:00',
|
||||||
|
...initialQueryBaseValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialNoteValues = {
|
||||||
|
text: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialCrawlerValues = {
|
||||||
|
extract_type: 'markdown',
|
||||||
|
query: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialInvokeValues = {
|
||||||
|
url: '',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 60,
|
||||||
|
headers: `{
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Connection": "keep-alive"
|
||||||
|
}`,
|
||||||
|
proxy: '',
|
||||||
|
clean_html: false,
|
||||||
|
variables: [],
|
||||||
|
outputs: {
|
||||||
|
result: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialTemplateValues = {
|
||||||
|
content: '',
|
||||||
|
parameters: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialEmailValues = {
|
||||||
|
smtp_server: '',
|
||||||
|
smtp_port: 465,
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
sender_name: '',
|
||||||
|
to_email: '',
|
||||||
|
cc_email: '',
|
||||||
|
subject: '',
|
||||||
|
content: '',
|
||||||
|
outputs: {
|
||||||
|
success: {
|
||||||
|
value: true,
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialIterationValues = {
|
||||||
|
items_ref: '',
|
||||||
|
outputs: {},
|
||||||
|
};
|
||||||
|
export const initialIterationStartValues = {
|
||||||
|
outputs: {
|
||||||
|
item: {
|
||||||
|
type: 'unkown',
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialCodeValues = {
|
||||||
|
lang: ProgrammingLanguage.Python,
|
||||||
|
script: CodeTemplateStrMap[ProgrammingLanguage.Python],
|
||||||
|
arguments: {
|
||||||
|
arg1: '',
|
||||||
|
arg2: '',
|
||||||
|
},
|
||||||
|
outputs: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialWaitingDialogueValues = {};
|
||||||
|
|
||||||
|
export const initialAgentValues = {
|
||||||
|
...initialLlmBaseValues,
|
||||||
|
description: '',
|
||||||
|
user_prompt: '',
|
||||||
|
sys_prompt: t('flow.sysPromptDefultValue'),
|
||||||
|
prompts: [{ role: PromptRole.User, content: `{${AgentGlobals.SysQuery}}` }],
|
||||||
|
message_history_window_size: 12,
|
||||||
|
max_retries: 3,
|
||||||
|
delay_after_error: 1,
|
||||||
|
visual_files_var: '',
|
||||||
|
max_rounds: 1,
|
||||||
|
exception_method: '',
|
||||||
|
exception_goto: [],
|
||||||
|
exception_default_value: '',
|
||||||
|
tools: [],
|
||||||
|
mcp: [],
|
||||||
|
cite: true,
|
||||||
|
outputs: {
|
||||||
|
// structured_output: {
|
||||||
|
// topic: {
|
||||||
|
// type: 'string',
|
||||||
|
// description:
|
||||||
|
// 'default:general. The category of the search.news is useful for retrieving real-time updates, particularly about politics, sports, and major current events covered by mainstream media sources. general is for broader, more general-purpose searches that may include a wide range of sources.',
|
||||||
|
// enum: ['general', 'news'],
|
||||||
|
// default: 'general',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
content: {
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialUserFillUpValues = {
|
||||||
|
enable_tips: true,
|
||||||
|
tips: '',
|
||||||
|
inputs: [],
|
||||||
|
outputs: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum StringTransformMethod {
|
||||||
|
Merge = 'merge',
|
||||||
|
Split = 'split',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum StringTransformDelimiter {
|
||||||
|
Comma = ',',
|
||||||
|
Semicolon = ';',
|
||||||
|
Period = '.',
|
||||||
|
LineBreak = '\n',
|
||||||
|
Tab = '\t',
|
||||||
|
Space = ' ',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialStringTransformValues = {
|
||||||
|
method: StringTransformMethod.Merge,
|
||||||
|
split_ref: '',
|
||||||
|
script: '',
|
||||||
|
delimiters: [StringTransformDelimiter.Comma],
|
||||||
|
outputs: {
|
||||||
|
result: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum TavilySearchDepth {
|
||||||
|
Basic = 'basic',
|
||||||
|
Advanced = 'advanced',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TavilyTopic {
|
||||||
|
News = 'news',
|
||||||
|
General = 'general',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialTavilyValues = {
|
||||||
|
api_key: '',
|
||||||
|
query: AgentGlobals.SysQuery,
|
||||||
|
search_depth: TavilySearchDepth.Basic,
|
||||||
|
topic: TavilyTopic.General,
|
||||||
|
max_results: 5,
|
||||||
|
days: 7,
|
||||||
|
include_answer: false,
|
||||||
|
include_raw_content: true,
|
||||||
|
include_images: false,
|
||||||
|
include_image_descriptions: false,
|
||||||
|
include_domains: [],
|
||||||
|
exclude_domains: [],
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
value: [],
|
||||||
|
type: 'Array<Object>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum TavilyExtractDepth {
|
||||||
|
Basic = 'basic',
|
||||||
|
Advanced = 'advanced',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TavilyExtractFormat {
|
||||||
|
Text = 'text',
|
||||||
|
Markdown = 'markdown',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialTavilyExtractValues = {
|
||||||
|
urls: '',
|
||||||
|
extract_depth: TavilyExtractDepth.Basic,
|
||||||
|
format: TavilyExtractFormat.Markdown,
|
||||||
|
outputs: {
|
||||||
|
formalized_content: {
|
||||||
|
value: '',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
value: [],
|
||||||
|
type: 'Array<Object>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CategorizeAnchorPointPositions = [
|
||||||
|
{ top: 1, right: 34 },
|
||||||
|
{ top: 8, right: 18 },
|
||||||
|
{ top: 15, right: 10 },
|
||||||
|
{ top: 24, right: 4 },
|
||||||
|
{ top: 31, right: 1 },
|
||||||
|
{ top: 38, right: -2 },
|
||||||
|
{ top: 62, right: -2 }, //bottom
|
||||||
|
{ top: 71, right: 1 },
|
||||||
|
{ top: 79, right: 6 },
|
||||||
|
{ top: 86, right: 12 },
|
||||||
|
{ top: 91, right: 20 },
|
||||||
|
{ top: 98, right: 34 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// key is the source of the edge, value is the target of the edge
|
||||||
|
// no connection lines are allowed between key and value
|
||||||
|
export const RestrictedUpstreamMap = {
|
||||||
|
[Operator.Begin]: [Operator.Relevant],
|
||||||
|
[Operator.Categorize]: [Operator.Begin, Operator.Categorize],
|
||||||
|
[Operator.Retrieval]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.Message]: [
|
||||||
|
Operator.Begin,
|
||||||
|
Operator.Message,
|
||||||
|
Operator.Retrieval,
|
||||||
|
Operator.RewriteQuestion,
|
||||||
|
Operator.Categorize,
|
||||||
|
],
|
||||||
|
[Operator.Relevant]: [Operator.Begin],
|
||||||
|
[Operator.RewriteQuestion]: [
|
||||||
|
Operator.Begin,
|
||||||
|
Operator.Message,
|
||||||
|
Operator.RewriteQuestion,
|
||||||
|
Operator.Relevant,
|
||||||
|
],
|
||||||
|
[Operator.KeywordExtract]: [
|
||||||
|
Operator.Begin,
|
||||||
|
Operator.Message,
|
||||||
|
Operator.Relevant,
|
||||||
|
],
|
||||||
|
[Operator.Baidu]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.DuckDuckGo]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.Wikipedia]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.PubMed]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.ArXiv]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.Google]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.Bing]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.GoogleScholar]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.DeepL]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.GitHub]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.BaiduFanyi]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.QWeather]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.SearXNG]: [Operator.Begin, Operator.Retrieval],
|
||||||
|
[Operator.ExeSQL]: [Operator.Begin],
|
||||||
|
[Operator.Switch]: [Operator.Begin],
|
||||||
|
[Operator.WenCai]: [Operator.Begin],
|
||||||
|
[Operator.AkShare]: [Operator.Begin],
|
||||||
|
[Operator.YahooFinance]: [Operator.Begin],
|
||||||
|
[Operator.Jin10]: [Operator.Begin],
|
||||||
|
[Operator.Concentrator]: [Operator.Begin],
|
||||||
|
[Operator.TuShare]: [Operator.Begin],
|
||||||
|
[Operator.Crawler]: [Operator.Begin],
|
||||||
|
[Operator.Note]: [],
|
||||||
|
[Operator.Invoke]: [Operator.Begin],
|
||||||
|
[Operator.Email]: [Operator.Begin],
|
||||||
|
[Operator.Iteration]: [Operator.Begin],
|
||||||
|
[Operator.IterationStart]: [Operator.Begin],
|
||||||
|
[Operator.Code]: [Operator.Begin],
|
||||||
|
[Operator.WaitingDialogue]: [Operator.Begin],
|
||||||
|
[Operator.Agent]: [Operator.Begin],
|
||||||
|
[Operator.TavilySearch]: [Operator.Begin],
|
||||||
|
[Operator.TavilyExtract]: [Operator.Begin],
|
||||||
|
[Operator.StringTransform]: [Operator.Begin],
|
||||||
|
[Operator.UserFillUp]: [Operator.Begin],
|
||||||
|
[Operator.Tool]: [Operator.Begin],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeMap = {
|
||||||
|
[Operator.Begin]: 'beginNode',
|
||||||
|
[Operator.Categorize]: 'categorizeNode',
|
||||||
|
[Operator.Retrieval]: 'retrievalNode',
|
||||||
|
[Operator.Message]: 'messageNode',
|
||||||
|
[Operator.Relevant]: 'relevantNode',
|
||||||
|
[Operator.RewriteQuestion]: 'rewriteNode',
|
||||||
|
[Operator.KeywordExtract]: 'keywordNode',
|
||||||
|
[Operator.DuckDuckGo]: 'ragNode',
|
||||||
|
[Operator.Baidu]: 'ragNode',
|
||||||
|
[Operator.Wikipedia]: 'ragNode',
|
||||||
|
[Operator.PubMed]: 'ragNode',
|
||||||
|
[Operator.ArXiv]: 'ragNode',
|
||||||
|
[Operator.Google]: 'ragNode',
|
||||||
|
[Operator.Bing]: 'ragNode',
|
||||||
|
[Operator.GoogleScholar]: 'ragNode',
|
||||||
|
[Operator.DeepL]: 'ragNode',
|
||||||
|
[Operator.GitHub]: 'ragNode',
|
||||||
|
[Operator.BaiduFanyi]: 'ragNode',
|
||||||
|
[Operator.QWeather]: 'ragNode',
|
||||||
|
[Operator.SearXNG]: 'ragNode',
|
||||||
|
[Operator.ExeSQL]: 'ragNode',
|
||||||
|
[Operator.Switch]: 'switchNode',
|
||||||
|
[Operator.Concentrator]: 'logicNode',
|
||||||
|
[Operator.WenCai]: 'ragNode',
|
||||||
|
[Operator.AkShare]: 'ragNode',
|
||||||
|
[Operator.YahooFinance]: 'ragNode',
|
||||||
|
[Operator.Jin10]: 'ragNode',
|
||||||
|
[Operator.TuShare]: 'ragNode',
|
||||||
|
[Operator.Note]: 'noteNode',
|
||||||
|
[Operator.Crawler]: 'ragNode',
|
||||||
|
[Operator.Invoke]: 'ragNode',
|
||||||
|
[Operator.Email]: 'ragNode',
|
||||||
|
[Operator.Iteration]: 'group',
|
||||||
|
[Operator.IterationStart]: 'iterationStartNode',
|
||||||
|
[Operator.Code]: 'ragNode',
|
||||||
|
[Operator.WaitingDialogue]: 'ragNode',
|
||||||
|
[Operator.Agent]: 'agentNode',
|
||||||
|
[Operator.Tool]: 'toolNode',
|
||||||
|
[Operator.TavilySearch]: 'ragNode',
|
||||||
|
[Operator.UserFillUp]: 'ragNode',
|
||||||
|
[Operator.StringTransform]: 'ragNode',
|
||||||
|
[Operator.TavilyExtract]: 'ragNode',
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
Operator.Concentrator,
|
||||||
|
Operator.Message,
|
||||||
|
Operator.RewriteQuestion,
|
||||||
|
Operator.Switch,
|
||||||
|
Operator.Iteration,
|
||||||
|
Operator.UserFillUp,
|
||||||
|
Operator.IterationStart,
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum NodeHandleId {
|
||||||
|
Start = 'start',
|
||||||
|
End = 'end',
|
||||||
|
Tool = 'tool',
|
||||||
|
AgentTop = 'agentTop',
|
||||||
|
AgentBottom = 'agentBottom',
|
||||||
|
AgentException = 'agentException',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VariableType {
|
||||||
|
String = 'string',
|
||||||
|
Array = 'array',
|
||||||
|
File = 'file',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AgentExceptionMethod {
|
||||||
|
Comment = 'comment',
|
||||||
|
Goto = 'goto',
|
||||||
|
}
|
||||||
50
web/src/pages/data-flow/context.ts
Normal file
50
web/src/pages/data-flow/context.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { HandleType, Position } from '@xyflow/react';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
import { useAddNode } from './hooks/use-add-node';
|
||||||
|
import { useCacheChatLog } from './hooks/use-cache-chat-log';
|
||||||
|
import { useShowFormDrawer, useShowLogSheet } from './hooks/use-show-drawer';
|
||||||
|
|
||||||
|
export const AgentFormContext = createContext<RAGFlowNodeType | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
type AgentInstanceContextType = Pick<
|
||||||
|
ReturnType<typeof useAddNode>,
|
||||||
|
'addCanvasNode'
|
||||||
|
> &
|
||||||
|
Pick<ReturnType<typeof useShowFormDrawer>, 'showFormDrawer'>;
|
||||||
|
|
||||||
|
export const AgentInstanceContext = createContext<AgentInstanceContextType>(
|
||||||
|
{} as AgentInstanceContextType,
|
||||||
|
);
|
||||||
|
|
||||||
|
type AgentChatContextType = Pick<
|
||||||
|
ReturnType<typeof useShowLogSheet>,
|
||||||
|
'showLogSheet'
|
||||||
|
> & { setLastSendLoadingFunc: (loading: boolean, messageId: string) => void };
|
||||||
|
|
||||||
|
export const AgentChatContext = createContext<AgentChatContextType>(
|
||||||
|
{} as AgentChatContextType,
|
||||||
|
);
|
||||||
|
|
||||||
|
type AgentChatLogContextType = Pick<
|
||||||
|
ReturnType<typeof useCacheChatLog>,
|
||||||
|
'addEventList' | 'setCurrentMessageId'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const AgentChatLogContext = createContext<AgentChatLogContextType>(
|
||||||
|
{} as AgentChatLogContextType,
|
||||||
|
);
|
||||||
|
|
||||||
|
export type HandleContextType = {
|
||||||
|
nodeId?: string;
|
||||||
|
id?: string;
|
||||||
|
type: HandleType;
|
||||||
|
position: Position;
|
||||||
|
isFromConnectionDrag: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HandleContext = createContext<HandleContextType>(
|
||||||
|
{} as HandleContextType,
|
||||||
|
);
|
||||||
260
web/src/pages/data-flow/debug-content/index.tsx
Normal file
260
web/src/pages/data-flow/debug-content/index.tsx
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import { ButtonLoading } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { RAGFlowSelect } from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { IMessage } from '@/pages/chat/interface';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import React, { ReactNode, useCallback, useMemo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { BeginQueryType } from '../constant';
|
||||||
|
import { BeginQuery } from '../interface';
|
||||||
|
import { FileUploadDirectUpload } from './uploader';
|
||||||
|
|
||||||
|
const StringFields = [
|
||||||
|
BeginQueryType.Line,
|
||||||
|
BeginQueryType.Paragraph,
|
||||||
|
BeginQueryType.Options,
|
||||||
|
];
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
parameters: BeginQuery[];
|
||||||
|
message?: IMessage;
|
||||||
|
ok(parameters: any[]): void;
|
||||||
|
isNext?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
submitButtonDisabled?: boolean;
|
||||||
|
btnText?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DebugContent = ({
|
||||||
|
parameters,
|
||||||
|
message,
|
||||||
|
ok,
|
||||||
|
isNext = true,
|
||||||
|
loading = false,
|
||||||
|
submitButtonDisabled = false,
|
||||||
|
btnText,
|
||||||
|
}: IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const formSchemaValues = useMemo(() => {
|
||||||
|
const obj = parameters.reduce<{
|
||||||
|
schema: Record<string, z.ZodType>;
|
||||||
|
values: Record<string, any>;
|
||||||
|
}>(
|
||||||
|
(pre, cur, idx) => {
|
||||||
|
const type = cur.type;
|
||||||
|
let fieldSchema;
|
||||||
|
let value;
|
||||||
|
if (StringFields.some((x) => x === type)) {
|
||||||
|
fieldSchema = z.string().trim().min(1);
|
||||||
|
} else if (type === BeginQueryType.Boolean) {
|
||||||
|
fieldSchema = z.boolean();
|
||||||
|
value = false;
|
||||||
|
} else if (type === BeginQueryType.Integer || type === 'float') {
|
||||||
|
fieldSchema = z.coerce.number();
|
||||||
|
} else {
|
||||||
|
fieldSchema = z.record(z.any());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cur.optional) {
|
||||||
|
fieldSchema = fieldSchema.optional();
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = idx.toString();
|
||||||
|
|
||||||
|
pre.schema[index] = fieldSchema;
|
||||||
|
pre.values[index] = value;
|
||||||
|
|
||||||
|
return pre;
|
||||||
|
},
|
||||||
|
{ schema: {}, values: {} },
|
||||||
|
);
|
||||||
|
|
||||||
|
return { schema: z.object(obj.schema), values: obj.values };
|
||||||
|
}, [parameters]);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchemaValues.schema>>({
|
||||||
|
defaultValues: formSchemaValues.values,
|
||||||
|
resolver: zodResolver(formSchemaValues.schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const submittable = true;
|
||||||
|
|
||||||
|
const renderWidget = useCallback(
|
||||||
|
(q: BeginQuery, idx: string) => {
|
||||||
|
const props = {
|
||||||
|
key: idx,
|
||||||
|
label: q.name ?? q.key,
|
||||||
|
name: idx,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BeginQueryTypeMap = {
|
||||||
|
[BeginQueryType.Line]: (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={props.name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{props.label}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field}></Input>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[BeginQueryType.Paragraph]: (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={props.name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{props.label}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea rows={1} {...field}></Textarea>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[BeginQueryType.Options]: (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={props.name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{props.label}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RAGFlowSelect
|
||||||
|
allowClear
|
||||||
|
options={
|
||||||
|
q.options?.map((x) => ({
|
||||||
|
label: x,
|
||||||
|
value: x as string,
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
{...field}
|
||||||
|
></RAGFlowSelect>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[BeginQueryType.File]: (
|
||||||
|
<React.Fragment key={idx}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={props.name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>{t('assistantAvatar')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<FileUploadDirectUpload
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
></FileUploadDirectUpload>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
),
|
||||||
|
[BeginQueryType.Integer]: (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={props.name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{props.label}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" {...field}></Input>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[BeginQueryType.Boolean]: (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={props.name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{props.label}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
></Switch>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
BeginQueryTypeMap[q.type as BeginQueryType] ??
|
||||||
|
BeginQueryTypeMap[BeginQueryType.Paragraph]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[form, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(values: z.infer<typeof formSchemaValues.schema>) => {
|
||||||
|
const nextValues = Object.entries(values).map(([key, value]) => {
|
||||||
|
const item = parameters[Number(key)];
|
||||||
|
return { ...item, value };
|
||||||
|
});
|
||||||
|
|
||||||
|
ok(nextValues);
|
||||||
|
},
|
||||||
|
[formSchemaValues, ok, parameters],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section>
|
||||||
|
{message?.data?.tips && <div className="mb-2">{message.data.tips}</div>}
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{parameters.map((x, idx) => {
|
||||||
|
return <div key={idx}>{renderWidget(x, idx.toString())}</div>;
|
||||||
|
})}
|
||||||
|
<div>
|
||||||
|
<ButtonLoading
|
||||||
|
type="submit"
|
||||||
|
loading={loading}
|
||||||
|
disabled={!submittable || submitButtonDisabled}
|
||||||
|
className="w-full mt-1"
|
||||||
|
>
|
||||||
|
{btnText || t(isNext ? 'common.next' : 'flow.run')}
|
||||||
|
</ButtonLoading>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DebugContent;
|
||||||
103
web/src/pages/data-flow/debug-content/popover-form.tsx
Normal file
103
web/src/pages/data-flow/debug-content/popover-form.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Popover, PopoverContent } from '@/components/ui/popover';
|
||||||
|
import { useParseDocument } from '@/hooks/document-hooks';
|
||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const reg =
|
||||||
|
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/;
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
url: z.string(),
|
||||||
|
result: z.any(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
url: '',
|
||||||
|
result: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PopoverForm = ({
|
||||||
|
children,
|
||||||
|
visible,
|
||||||
|
switchVisible,
|
||||||
|
}: PropsWithChildren<IModalProps<any>>) => {
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: values,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
});
|
||||||
|
const { parseDocument, loading } = useParseDocument();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// useResetFormOnCloseModal({
|
||||||
|
// form,
|
||||||
|
// visible,
|
||||||
|
// });
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof FormSchema>) {
|
||||||
|
const val = values.url;
|
||||||
|
|
||||||
|
if (reg.test(val)) {
|
||||||
|
const ret = await parseDocument(val);
|
||||||
|
if (ret?.data?.code === 0) {
|
||||||
|
form.setValue('result', ret?.data?.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`url`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
// onPressEnter={(e) => e.preventDefault()}
|
||||||
|
placeholder={t('flow.pasteFileLink')}
|
||||||
|
// suffix={
|
||||||
|
// <Button
|
||||||
|
// type="primary"
|
||||||
|
// onClick={onOk}
|
||||||
|
// size={'small'}
|
||||||
|
// loading={loading}
|
||||||
|
// >
|
||||||
|
// {t('common.submit')}
|
||||||
|
// </Button>
|
||||||
|
// }
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`result`}
|
||||||
|
render={() => <></>}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={visible} onOpenChange={switchVisible}>
|
||||||
|
{children}
|
||||||
|
<PopoverContent>{content}</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
116
web/src/pages/data-flow/debug-content/uploader.tsx
Normal file
116
web/src/pages/data-flow/debug-content/uploader.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FileUpload,
|
||||||
|
FileUploadDropzone,
|
||||||
|
FileUploadItem,
|
||||||
|
FileUploadItemDelete,
|
||||||
|
FileUploadItemMetadata,
|
||||||
|
FileUploadItemPreview,
|
||||||
|
FileUploadItemProgress,
|
||||||
|
FileUploadList,
|
||||||
|
FileUploadTrigger,
|
||||||
|
type FileUploadProps,
|
||||||
|
} from '@/components/file-upload';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useUploadCanvasFile } from '@/hooks/use-agent-request';
|
||||||
|
import { Upload, X } from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
type FileUploadDirectUploadProps = {
|
||||||
|
value: Record<string, any>;
|
||||||
|
onChange(value: Record<string, any>): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FileUploadDirectUpload({
|
||||||
|
onChange,
|
||||||
|
}: FileUploadDirectUploadProps) {
|
||||||
|
const [files, setFiles] = React.useState<File[]>([]);
|
||||||
|
|
||||||
|
const { uploadCanvasFile } = useUploadCanvasFile();
|
||||||
|
|
||||||
|
const onUpload: NonNullable<FileUploadProps['onUpload']> = React.useCallback(
|
||||||
|
async (files, { onSuccess, onError }) => {
|
||||||
|
try {
|
||||||
|
const uploadPromises = files.map(async (file) => {
|
||||||
|
const handleError = (error?: any) => {
|
||||||
|
onError(
|
||||||
|
file,
|
||||||
|
error instanceof Error ? error : new Error('Upload failed'),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const ret = await uploadCanvasFile([file]);
|
||||||
|
if (ret.code === 0) {
|
||||||
|
onSuccess(file);
|
||||||
|
onChange(ret.data);
|
||||||
|
} else {
|
||||||
|
handleError();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for all uploads to complete
|
||||||
|
await Promise.all(uploadPromises);
|
||||||
|
} catch (error) {
|
||||||
|
// This handles any error that might occur outside the individual upload processes
|
||||||
|
console.error('Unexpected error during upload:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange, uploadCanvasFile],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFileReject = React.useCallback((file: File, message: string) => {
|
||||||
|
toast(message, {
|
||||||
|
description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileUpload
|
||||||
|
value={files}
|
||||||
|
onValueChange={setFiles}
|
||||||
|
onUpload={onUpload}
|
||||||
|
onFileReject={onFileReject}
|
||||||
|
maxFiles={1}
|
||||||
|
className="w-full"
|
||||||
|
multiple={false}
|
||||||
|
>
|
||||||
|
<FileUploadDropzone>
|
||||||
|
<div className="flex flex-col items-center gap-1 text-center">
|
||||||
|
<div className="flex items-center justify-center rounded-full border p-2.5">
|
||||||
|
<Upload className="size-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<p className="font-medium text-sm">Drag & drop files here</p>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
Or click to browse (max 2 files)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FileUploadTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="mt-2 w-fit">
|
||||||
|
Browse files
|
||||||
|
</Button>
|
||||||
|
</FileUploadTrigger>
|
||||||
|
</FileUploadDropzone>
|
||||||
|
<FileUploadList>
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<FileUploadItem key={index} value={file} className="flex-col">
|
||||||
|
<div className="flex w-full items-center gap-2">
|
||||||
|
<FileUploadItemPreview />
|
||||||
|
<FileUploadItemMetadata />
|
||||||
|
<FileUploadItemDelete asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="size-7">
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</FileUploadItemDelete>
|
||||||
|
</div>
|
||||||
|
<FileUploadItemProgress />
|
||||||
|
</FileUploadItem>
|
||||||
|
))}
|
||||||
|
</FileUploadList>
|
||||||
|
</FileUpload>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
web/src/pages/data-flow/flow-tooltip.tsx
Normal file
19
web/src/pages/data-flow/flow-tooltip.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const RunTooltip = ({ children }: PropsWithChildren) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>{children}</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t('flow.testRun')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
web/src/pages/data-flow/form-hooks.ts
Normal file
43
web/src/pages/data-flow/form-hooks.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { Operator, RestrictedUpstreamMap } from './constant';
|
||||||
|
import useGraphStore from './store';
|
||||||
|
|
||||||
|
export const useBuildFormSelectOptions = (
|
||||||
|
operatorName: Operator,
|
||||||
|
selfId?: string, // exclude the current node
|
||||||
|
) => {
|
||||||
|
const nodes = useGraphStore((state) => state.nodes);
|
||||||
|
|
||||||
|
const buildCategorizeToOptions = useCallback(
|
||||||
|
(toList: string[]) => {
|
||||||
|
const excludedNodes: Operator[] = [
|
||||||
|
Operator.Note,
|
||||||
|
...(RestrictedUpstreamMap[operatorName] ?? []),
|
||||||
|
];
|
||||||
|
return nodes
|
||||||
|
.filter(
|
||||||
|
(x) =>
|
||||||
|
excludedNodes.every((y) => y !== x.data.label) &&
|
||||||
|
x.id !== selfId &&
|
||||||
|
!toList.some((y) => y === x.id), // filter out selected values in other to fields from the current drop-down box options
|
||||||
|
)
|
||||||
|
.map((x) => ({ label: x.data.name, value: x.id }));
|
||||||
|
},
|
||||||
|
[nodes, operatorName, selfId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildCategorizeToOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBuildSortOptions = () => {
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return ['data', 'relevance'].map((x) => ({
|
||||||
|
value: x,
|
||||||
|
label: t(x),
|
||||||
|
}));
|
||||||
|
}, [t]);
|
||||||
|
return options;
|
||||||
|
};
|
||||||
165
web/src/pages/data-flow/form-sheet/form-config-map.tsx
Normal file
165
web/src/pages/data-flow/form-sheet/form-config-map.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { Operator } from '../constant';
|
||||||
|
import AgentForm from '../form/agent-form';
|
||||||
|
import AkShareForm from '../form/akshare-form';
|
||||||
|
import ArXivForm from '../form/arxiv-form';
|
||||||
|
import BaiduFanyiForm from '../form/baidu-fanyi-form';
|
||||||
|
import BaiduForm from '../form/baidu-form';
|
||||||
|
import BeginForm from '../form/begin-form';
|
||||||
|
import BingForm from '../form/bing-form';
|
||||||
|
import CategorizeForm from '../form/categorize-form';
|
||||||
|
import CodeForm from '../form/code-form';
|
||||||
|
import CrawlerForm from '../form/crawler-form';
|
||||||
|
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 GithubForm from '../form/github-form';
|
||||||
|
import GoogleForm from '../form/google-form';
|
||||||
|
import GoogleScholarForm from '../form/google-scholar-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 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 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 TuShareForm from '../form/tushare-form';
|
||||||
|
import UserFillUpForm from '../form/user-fill-up-form';
|
||||||
|
import WenCaiForm from '../form/wencai-form';
|
||||||
|
import WikipediaForm from '../form/wikipedia-form';
|
||||||
|
import YahooFinanceForm from '../form/yahoo-finance-form';
|
||||||
|
|
||||||
|
export const FormConfigMap = {
|
||||||
|
[Operator.Begin]: {
|
||||||
|
component: BeginForm,
|
||||||
|
},
|
||||||
|
[Operator.Retrieval]: {
|
||||||
|
component: RetrievalForm,
|
||||||
|
},
|
||||||
|
[Operator.Categorize]: {
|
||||||
|
component: CategorizeForm,
|
||||||
|
},
|
||||||
|
[Operator.Message]: {
|
||||||
|
component: MessageForm,
|
||||||
|
},
|
||||||
|
[Operator.Relevant]: {
|
||||||
|
component: RelevantForm,
|
||||||
|
},
|
||||||
|
[Operator.RewriteQuestion]: {
|
||||||
|
component: RewriteQuestionForm,
|
||||||
|
},
|
||||||
|
[Operator.Code]: {
|
||||||
|
component: CodeForm,
|
||||||
|
},
|
||||||
|
[Operator.WaitingDialogue]: {
|
||||||
|
component: CodeForm,
|
||||||
|
},
|
||||||
|
[Operator.Agent]: {
|
||||||
|
component: AgentForm,
|
||||||
|
},
|
||||||
|
[Operator.Baidu]: {
|
||||||
|
component: BaiduForm,
|
||||||
|
},
|
||||||
|
[Operator.DuckDuckGo]: {
|
||||||
|
component: DuckDuckGoForm,
|
||||||
|
},
|
||||||
|
[Operator.KeywordExtract]: {
|
||||||
|
component: KeywordExtractForm,
|
||||||
|
},
|
||||||
|
[Operator.Wikipedia]: {
|
||||||
|
component: WikipediaForm,
|
||||||
|
},
|
||||||
|
[Operator.PubMed]: {
|
||||||
|
component: PubMedForm,
|
||||||
|
},
|
||||||
|
[Operator.ArXiv]: {
|
||||||
|
component: ArXivForm,
|
||||||
|
},
|
||||||
|
[Operator.Google]: {
|
||||||
|
component: GoogleForm,
|
||||||
|
},
|
||||||
|
[Operator.Bing]: {
|
||||||
|
component: BingForm,
|
||||||
|
},
|
||||||
|
[Operator.GoogleScholar]: {
|
||||||
|
component: GoogleScholarForm,
|
||||||
|
},
|
||||||
|
[Operator.DeepL]: {
|
||||||
|
component: DeepLForm,
|
||||||
|
},
|
||||||
|
[Operator.GitHub]: {
|
||||||
|
component: GithubForm,
|
||||||
|
},
|
||||||
|
[Operator.BaiduFanyi]: {
|
||||||
|
component: BaiduFanyiForm,
|
||||||
|
},
|
||||||
|
[Operator.QWeather]: {
|
||||||
|
component: QWeatherForm,
|
||||||
|
},
|
||||||
|
[Operator.ExeSQL]: {
|
||||||
|
component: ExeSQLForm,
|
||||||
|
},
|
||||||
|
[Operator.Switch]: {
|
||||||
|
component: SwitchForm,
|
||||||
|
},
|
||||||
|
[Operator.WenCai]: {
|
||||||
|
component: WenCaiForm,
|
||||||
|
},
|
||||||
|
[Operator.AkShare]: {
|
||||||
|
component: AkShareForm,
|
||||||
|
},
|
||||||
|
[Operator.YahooFinance]: {
|
||||||
|
component: YahooFinanceForm,
|
||||||
|
},
|
||||||
|
[Operator.Jin10]: {
|
||||||
|
component: Jin10Form,
|
||||||
|
},
|
||||||
|
[Operator.TuShare]: {
|
||||||
|
component: TuShareForm,
|
||||||
|
},
|
||||||
|
[Operator.Crawler]: {
|
||||||
|
component: CrawlerForm,
|
||||||
|
},
|
||||||
|
[Operator.Invoke]: {
|
||||||
|
component: InvokeForm,
|
||||||
|
},
|
||||||
|
[Operator.SearXNG]: {
|
||||||
|
component: SearXNGForm,
|
||||||
|
},
|
||||||
|
[Operator.Concentrator]: {
|
||||||
|
component: () => <></>,
|
||||||
|
},
|
||||||
|
[Operator.Note]: {
|
||||||
|
component: () => <></>,
|
||||||
|
},
|
||||||
|
[Operator.Email]: {
|
||||||
|
component: EmailForm,
|
||||||
|
},
|
||||||
|
[Operator.Iteration]: {
|
||||||
|
component: IterationForm,
|
||||||
|
},
|
||||||
|
[Operator.IterationStart]: {
|
||||||
|
component: IterationStartForm,
|
||||||
|
},
|
||||||
|
[Operator.TavilySearch]: {
|
||||||
|
component: TavilyForm,
|
||||||
|
},
|
||||||
|
[Operator.UserFillUp]: {
|
||||||
|
component: UserFillUpForm,
|
||||||
|
},
|
||||||
|
[Operator.StringTransform]: {
|
||||||
|
component: StringTransformForm,
|
||||||
|
},
|
||||||
|
[Operator.TavilyExtract]: {
|
||||||
|
component: TavilyExtractForm,
|
||||||
|
},
|
||||||
|
};
|
||||||
134
web/src/pages/data-flow/form-sheet/next.tsx
Normal file
134
web/src/pages/data-flow/form-sheet/next.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from '@/components/ui/sheet';
|
||||||
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { lowerFirst } from 'lodash';
|
||||||
|
import { Play, X } from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { BeginId, Operator } from '../constant';
|
||||||
|
import { AgentFormContext } from '../context';
|
||||||
|
import { RunTooltip } from '../flow-tooltip';
|
||||||
|
import { useHandleNodeNameChange } from '../hooks/use-change-node-name';
|
||||||
|
import OperatorIcon from '../operator-icon';
|
||||||
|
import useGraphStore from '../store';
|
||||||
|
import { needsSingleStepDebugging } from '../utils';
|
||||||
|
import { FormConfigMap } from './form-config-map';
|
||||||
|
import SingleDebugSheet from './single-debug-sheet';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node?: RAGFlowNodeType;
|
||||||
|
singleDebugDrawerVisible: IModalProps<any>['visible'];
|
||||||
|
hideSingleDebugDrawer: IModalProps<any>['hideModal'];
|
||||||
|
showSingleDebugDrawer: IModalProps<any>['showModal'];
|
||||||
|
chatVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmptyContent = () => <div></div>;
|
||||||
|
|
||||||
|
const FormSheet = ({
|
||||||
|
visible,
|
||||||
|
hideModal,
|
||||||
|
node,
|
||||||
|
singleDebugDrawerVisible,
|
||||||
|
chatVisible,
|
||||||
|
hideSingleDebugDrawer,
|
||||||
|
showSingleDebugDrawer,
|
||||||
|
}: IModalProps<any> & IProps) => {
|
||||||
|
const operatorName: Operator = node?.data.label as Operator;
|
||||||
|
const clickedToolId = useGraphStore((state) => state.clickedToolId);
|
||||||
|
|
||||||
|
const currentFormMap = FormConfigMap[operatorName];
|
||||||
|
|
||||||
|
const OperatorForm = currentFormMap?.component ?? EmptyContent;
|
||||||
|
|
||||||
|
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
|
||||||
|
id: node?.id,
|
||||||
|
data: node?.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMcp = useMemo(() => {
|
||||||
|
return (
|
||||||
|
operatorName === Operator.Tool &&
|
||||||
|
Object.values(Operator).every((x) => x !== clickedToolId)
|
||||||
|
);
|
||||||
|
}, [clickedToolId, operatorName]);
|
||||||
|
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={visible} modal={false}>
|
||||||
|
<SheetContent
|
||||||
|
className={cn('top-20 p-0 flex flex-col pb-20 ', {
|
||||||
|
'right-[620px]': chatVisible,
|
||||||
|
})}
|
||||||
|
closeIcon={false}
|
||||||
|
>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle className="hidden"></SheetTitle>
|
||||||
|
<section className="flex-col border-b py-2 px-5">
|
||||||
|
<div className="flex items-center gap-2 pb-3">
|
||||||
|
<OperatorIcon name={operatorName}></OperatorIcon>
|
||||||
|
|
||||||
|
{isMcp ? (
|
||||||
|
<div className="flex-1">MCP Config</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1 flex-1">
|
||||||
|
<label htmlFor="">{t('title')}</label>
|
||||||
|
{node?.id === BeginId ? (
|
||||||
|
<span>{t(BeginId)}</span>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onBlur={handleNameBlur}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
></Input>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{needsSingleStepDebugging(operatorName) && (
|
||||||
|
<RunTooltip>
|
||||||
|
<Play
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
onClick={showSingleDebugDrawer}
|
||||||
|
/>
|
||||||
|
</RunTooltip>
|
||||||
|
)}
|
||||||
|
<X onClick={hideModal} />
|
||||||
|
</div>
|
||||||
|
{isMcp || (
|
||||||
|
<span>
|
||||||
|
{t(
|
||||||
|
`${lowerFirst(operatorName === Operator.Tool ? clickedToolId : operatorName)}Description`,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</SheetHeader>
|
||||||
|
<section className="pt-4 overflow-auto flex-1">
|
||||||
|
{visible && (
|
||||||
|
<AgentFormContext.Provider value={node}>
|
||||||
|
<OperatorForm node={node} key={node?.id}></OperatorForm>
|
||||||
|
</AgentFormContext.Provider>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</SheetContent>
|
||||||
|
{singleDebugDrawerVisible && (
|
||||||
|
<SingleDebugSheet
|
||||||
|
visible={singleDebugDrawerVisible}
|
||||||
|
hideModal={hideSingleDebugDrawer}
|
||||||
|
componentId={node?.id}
|
||||||
|
></SingleDebugSheet>
|
||||||
|
)}
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormSheet;
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||||
|
import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet';
|
||||||
|
import { useDebugSingle, useFetchInputForm } from '@/hooks/use-agent-request';
|
||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import JsonView from 'react18-json-view';
|
||||||
|
import 'react18-json-view/src/style.css';
|
||||||
|
import DebugContent from '../../debug-content';
|
||||||
|
import { transferInputsArrayToObject } from '../../form/begin-form/use-watch-change';
|
||||||
|
import { buildBeginInputListFromObject } from '../../form/begin-form/utils';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
componentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SingleDebugSheet = ({
|
||||||
|
componentId,
|
||||||
|
visible,
|
||||||
|
hideModal,
|
||||||
|
}: IModalProps<any> & IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const inputForm = useFetchInputForm(componentId);
|
||||||
|
const { debugSingle, data, loading } = useDebugSingle();
|
||||||
|
|
||||||
|
const list = useMemo(() => {
|
||||||
|
return buildBeginInputListFromObject(inputForm);
|
||||||
|
}, [inputForm]);
|
||||||
|
|
||||||
|
const onOk = useCallback(
|
||||||
|
(nextValues: any[]) => {
|
||||||
|
if (componentId) {
|
||||||
|
debugSingle({
|
||||||
|
component_id: componentId,
|
||||||
|
params: transferInputsArrayToObject(nextValues),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[componentId, debugSingle],
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = JSON.stringify(data, null, 2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={visible} modal={false}>
|
||||||
|
<SheetContent className="top-20 p-0" closeIcon={false}>
|
||||||
|
<SheetHeader className="py-2 px-5">
|
||||||
|
<div className="flex justify-between ">
|
||||||
|
{t('flow.testRun')}
|
||||||
|
<X onClick={hideModal} className="cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
</SheetHeader>
|
||||||
|
<section className="overflow-y-auto pt-4 px-5">
|
||||||
|
<DebugContent
|
||||||
|
parameters={list}
|
||||||
|
ok={onOk}
|
||||||
|
isNext={false}
|
||||||
|
loading={loading}
|
||||||
|
submitButtonDisabled={list.length === 0}
|
||||||
|
></DebugContent>
|
||||||
|
{!isEmpty(data) ? (
|
||||||
|
<div
|
||||||
|
className={cn('mt-4 rounded-md border', {
|
||||||
|
[`border-state-error`]: !isEmpty(data._ERROR),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between p-2">
|
||||||
|
<span>JSON</span>
|
||||||
|
<CopyToClipboard text={content}></CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
<JsonView
|
||||||
|
src={data}
|
||||||
|
displaySize
|
||||||
|
collapseStringsAfterLength={100000000000}
|
||||||
|
className="w-full h-[800px] break-words overflow-auto p-2"
|
||||||
|
dark
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SingleDebugSheet;
|
||||||
191
web/src/pages/data-flow/form/agent-form/agent-tools.tsx
Normal file
191
web/src/pages/data-flow/form/agent-form/agent-tools.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { BlockButton } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Position } from '@xyflow/react';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { PencilLine, X } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
MouseEventHandler,
|
||||||
|
PropsWithChildren,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
import { Operator } from '../../constant';
|
||||||
|
import { AgentInstanceContext } from '../../context';
|
||||||
|
import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import OperatorIcon from '../../operator-icon';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
import { filterDownstreamAgentNodeIds } from '../../utils/filter-downstream-nodes';
|
||||||
|
import { ToolPopover } from './tool-popover';
|
||||||
|
import { useDeleteAgentNodeMCP } from './tool-popover/use-update-mcp';
|
||||||
|
import { useDeleteAgentNodeTools } from './tool-popover/use-update-tools';
|
||||||
|
import { useGetAgentMCPIds, useGetAgentToolNames } from './use-get-tools';
|
||||||
|
|
||||||
|
export function ToolCard({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren & React.HTMLAttributes<HTMLLIElement>) {
|
||||||
|
const element = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'flex bg-bg-card p-1 rounded-sm justify-between',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}, [children, className, props]);
|
||||||
|
|
||||||
|
if (children === Operator.Code) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{element}</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>It doesn't have any config.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionButtonProps<T> = {
|
||||||
|
record: T;
|
||||||
|
deleteRecord(record: T): void;
|
||||||
|
edit: MouseEventHandler<HTMLOrSVGElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ActionButton<T>({ deleteRecord, record, edit }: ActionButtonProps<T>) {
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
deleteRecord(record);
|
||||||
|
}, [deleteRecord, record]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-text-secondary">
|
||||||
|
<PencilLine
|
||||||
|
className="size-4 cursor-pointer"
|
||||||
|
data-tool={record}
|
||||||
|
onClick={edit}
|
||||||
|
/>
|
||||||
|
<X className="size-4 cursor-pointer" onClick={handleDelete} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentTools() {
|
||||||
|
const { toolNames } = useGetAgentToolNames();
|
||||||
|
const { deleteNodeTool } = useDeleteAgentNodeTools();
|
||||||
|
const { mcpIds } = useGetAgentMCPIds();
|
||||||
|
const { findMcpById } = useFindMcpById();
|
||||||
|
const { deleteNodeMCP } = useDeleteAgentNodeMCP();
|
||||||
|
const { showFormDrawer } = useContext(AgentInstanceContext);
|
||||||
|
const { clickedNodeId, findAgentToolNodeById, selectNodeIds } = useGraphStore(
|
||||||
|
(state) => state,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEdit: MouseEventHandler<SVGSVGElement> = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const toolNodeId = findAgentToolNodeById(clickedNodeId);
|
||||||
|
if (toolNodeId) {
|
||||||
|
selectNodeIds([toolNodeId]);
|
||||||
|
showFormDrawer(e, toolNodeId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[clickedNodeId, findAgentToolNodeById, selectNodeIds, showFormDrawer],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-2.5">
|
||||||
|
<span className="text-text-secondary">{t('flow.tools')}</span>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{toolNames.map((x) => (
|
||||||
|
<ToolCard key={x}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<OperatorIcon name={x as Operator}></OperatorIcon>
|
||||||
|
{x}
|
||||||
|
</div>
|
||||||
|
<ActionButton
|
||||||
|
record={x}
|
||||||
|
deleteRecord={deleteNodeTool(x)}
|
||||||
|
edit={handleEdit}
|
||||||
|
></ActionButton>
|
||||||
|
</ToolCard>
|
||||||
|
))}
|
||||||
|
{mcpIds.map((id) => (
|
||||||
|
<ToolCard key={id}>
|
||||||
|
{findMcpById(id)?.name}
|
||||||
|
<ActionButton
|
||||||
|
record={id}
|
||||||
|
deleteRecord={deleteNodeMCP(id)}
|
||||||
|
edit={handleEdit}
|
||||||
|
></ActionButton>
|
||||||
|
</ToolCard>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<ToolPopover>
|
||||||
|
<BlockButton>{t('flow.addTools')}</BlockButton>
|
||||||
|
</ToolPopover>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Agents({ node }: INextOperatorForm) {
|
||||||
|
const { addCanvasNode } = useContext(AgentInstanceContext);
|
||||||
|
const { deleteAgentDownstreamNodesById, edges, getNode, selectNodeIds } =
|
||||||
|
useGraphStore((state) => state);
|
||||||
|
const { showFormDrawer } = useContext(AgentInstanceContext);
|
||||||
|
|
||||||
|
const handleEdit = useCallback(
|
||||||
|
(nodeId: string): MouseEventHandler<SVGSVGElement> =>
|
||||||
|
(e) => {
|
||||||
|
selectNodeIds([nodeId]);
|
||||||
|
showFormDrawer(e, nodeId);
|
||||||
|
},
|
||||||
|
[selectNodeIds, showFormDrawer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const subBottomAgentNodeIds = useMemo(() => {
|
||||||
|
return filterDownstreamAgentNodeIds(edges, node?.id);
|
||||||
|
}, [edges, node?.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-2.5">
|
||||||
|
<span className="text-text-secondary">{t('flow.agent')}</span>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{subBottomAgentNodeIds.map((id) => {
|
||||||
|
const currentNode = getNode(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolCard key={id}>
|
||||||
|
{currentNode?.data.name}
|
||||||
|
<ActionButton
|
||||||
|
record={id}
|
||||||
|
deleteRecord={deleteAgentDownstreamNodesById}
|
||||||
|
edit={handleEdit(id)}
|
||||||
|
></ActionButton>
|
||||||
|
</ToolCard>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<BlockButton
|
||||||
|
onClick={addCanvasNode(Operator.Agent, {
|
||||||
|
nodeId: node?.id,
|
||||||
|
position: Position.Bottom,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('flow.addAgent')}
|
||||||
|
</BlockButton>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
web/src/pages/data-flow/form/agent-form/dynamic-prompt.tsx
Normal file
93
web/src/pages/data-flow/form/agent-form/dynamic-prompt.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { BlockButton, Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { RAGFlowSelect } from '@/components/ui/select';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PromptRole } from '../../constant';
|
||||||
|
import { PromptEditor } from '../components/prompt-editor';
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ label: 'User', value: PromptRole.User },
|
||||||
|
{ label: 'Assistant', value: PromptRole.Assistant },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DynamicPrompt = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext();
|
||||||
|
const name = 'prompts';
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex">
|
||||||
|
<div className="space-y-2 flex-1">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`${name}.${index}.role`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-1/3">
|
||||||
|
<FormLabel />
|
||||||
|
<FormControl>
|
||||||
|
<RAGFlowSelect
|
||||||
|
{...field}
|
||||||
|
options={options}
|
||||||
|
></RAGFlowSelect>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`${name}.${index}.content`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<section>
|
||||||
|
<PromptEditor
|
||||||
|
{...field}
|
||||||
|
showToolbar={false}
|
||||||
|
></PromptEditor>
|
||||||
|
</section>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
<BlockButton
|
||||||
|
onClick={() => append({ content: '', role: PromptRole.User })}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</BlockButton>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(DynamicPrompt);
|
||||||
63
web/src/pages/data-flow/form/agent-form/dynamic-tool.tsx
Normal file
63
web/src/pages/data-flow/form/agent-form/dynamic-tool.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { BlockButton, Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { PromptEditor } from '../components/prompt-editor';
|
||||||
|
|
||||||
|
const DynamicTool = () => {
|
||||||
|
const form = useFormContext();
|
||||||
|
const name = 'tools';
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex">
|
||||||
|
<div className="space-y-2 flex-1">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`${name}.${index}.component_name`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<section>
|
||||||
|
<PromptEditor
|
||||||
|
{...field}
|
||||||
|
showToolbar={false}
|
||||||
|
></PromptEditor>
|
||||||
|
</section>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
<BlockButton onClick={() => append({ component_name: '' })}>
|
||||||
|
Add
|
||||||
|
</BlockButton>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(DynamicTool);
|
||||||
280
web/src/pages/data-flow/form/agent-form/index.tsx
Normal file
280
web/src/pages/data-flow/form/agent-form/index.tsx
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import { Collapse } from '@/components/collapse';
|
||||||
|
import { FormContainer } from '@/components/form-container';
|
||||||
|
import {
|
||||||
|
LargeModelFilterFormSchema,
|
||||||
|
LargeModelFormField,
|
||||||
|
} from '@/components/large-model-form-field';
|
||||||
|
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
|
||||||
|
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
|
||||||
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input, NumberInput } from '@/components/ui/input';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { LlmModelType } from '@/constants/knowledge';
|
||||||
|
import { useFindLlmByUuid } from '@/hooks/use-llm-request';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { memo, useEffect, useMemo } from 'react';
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
AgentExceptionMethod,
|
||||||
|
NodeHandleId,
|
||||||
|
VariableType,
|
||||||
|
initialAgentValues,
|
||||||
|
} from '../../constant';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
import { isBottomSubAgent } from '../../utils';
|
||||||
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
|
import { DescriptionField } from '../components/description-field';
|
||||||
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
|
import { Output } from '../components/output';
|
||||||
|
import { PromptEditor } from '../components/prompt-editor';
|
||||||
|
import { QueryVariable } from '../components/query-variable';
|
||||||
|
import { AgentTools, Agents } from './agent-tools';
|
||||||
|
import { useValues } from './use-values';
|
||||||
|
import { useWatchFormChange } from './use-watch-change';
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
sys_prompt: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
user_prompt: z.string().optional(),
|
||||||
|
prompts: z.string().optional(),
|
||||||
|
// prompts: z
|
||||||
|
// .array(
|
||||||
|
// z.object({
|
||||||
|
// role: z.string(),
|
||||||
|
// content: z.string(),
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// .optional(),
|
||||||
|
message_history_window_size: z.coerce.number(),
|
||||||
|
tools: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
component_name: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
...LlmSettingSchema,
|
||||||
|
max_retries: z.coerce.number(),
|
||||||
|
delay_after_error: z.coerce.number().optional(),
|
||||||
|
visual_files_var: z.string().optional(),
|
||||||
|
max_rounds: z.coerce.number().optional(),
|
||||||
|
exception_method: z.string().optional(),
|
||||||
|
exception_goto: z.array(z.string()).optional(),
|
||||||
|
exception_default_value: z.string().optional(),
|
||||||
|
...LargeModelFilterFormSchema,
|
||||||
|
cite: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputList = buildOutputList(initialAgentValues.outputs);
|
||||||
|
|
||||||
|
function AgentForm({ node }: INextOperatorForm) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { edges, deleteEdgesBySourceAndSourceHandle } = useGraphStore(
|
||||||
|
(state) => state,
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultValues = useValues(node);
|
||||||
|
|
||||||
|
const ExceptionMethodOptions = Object.values(AgentExceptionMethod).map(
|
||||||
|
(x) => ({
|
||||||
|
label: t(`flow.${x}`),
|
||||||
|
value: x,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSubAgent = useMemo(() => {
|
||||||
|
return isBottomSubAgent(edges, node?.id);
|
||||||
|
}, [edges, node?.id]);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
defaultValues: defaultValues,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const llmId = useWatch({ control: form.control, name: 'llm_id' });
|
||||||
|
|
||||||
|
const findLlmByUuid = useFindLlmByUuid();
|
||||||
|
|
||||||
|
const exceptionMethod = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: 'exception_method',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (exceptionMethod !== AgentExceptionMethod.Goto) {
|
||||||
|
if (node?.id) {
|
||||||
|
deleteEdgesBySourceAndSourceHandle(
|
||||||
|
node?.id,
|
||||||
|
NodeHandleId.AgentException,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [deleteEdgesBySourceAndSourceHandle, exceptionMethod, node?.id]);
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<FormWrapper>
|
||||||
|
<FormContainer>
|
||||||
|
{isSubAgent && <DescriptionField></DescriptionField>}
|
||||||
|
<LargeModelFormField showSpeech2TextModel></LargeModelFormField>
|
||||||
|
{findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && (
|
||||||
|
<QueryVariable
|
||||||
|
name="visual_files_var"
|
||||||
|
label="Visual Input File"
|
||||||
|
type={VariableType.File}
|
||||||
|
></QueryVariable>
|
||||||
|
)}
|
||||||
|
</FormContainer>
|
||||||
|
|
||||||
|
<FormContainer>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`sys_prompt`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{t('flow.systemPrompt')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<PromptEditor
|
||||||
|
{...field}
|
||||||
|
placeholder={t('flow.messagePlaceholder')}
|
||||||
|
showToolbar={false}
|
||||||
|
></PromptEditor>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormContainer>
|
||||||
|
{isSubAgent || (
|
||||||
|
<FormContainer>
|
||||||
|
{/* <DynamicPrompt></DynamicPrompt> */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`prompts`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{t('flow.userPrompt')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<section>
|
||||||
|
<PromptEditor
|
||||||
|
{...field}
|
||||||
|
showToolbar={false}
|
||||||
|
></PromptEditor>
|
||||||
|
</section>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormContainer>
|
||||||
|
<AgentTools></AgentTools>
|
||||||
|
<Agents node={node}></Agents>
|
||||||
|
</FormContainer>
|
||||||
|
<Collapse title={<div>{t('flow.advancedSettings')}</div>}>
|
||||||
|
<FormContainer>
|
||||||
|
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`cite`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel tooltip={t('flow.citeTip')}>
|
||||||
|
{t('flow.cite')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
></Switch>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`max_retries`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{t('flow.maxRetries')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput {...field} max={8}></NumberInput>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`delay_after_error`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{t('flow.delayEfterError')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput {...field} max={5} step={0.1}></NumberInput>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`max_rounds`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{t('flow.maxRounds')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput {...field}></NumberInput>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`exception_method`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{t('flow.exceptionMethod')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SelectWithSearch
|
||||||
|
{...field}
|
||||||
|
options={ExceptionMethodOptions}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{exceptionMethod === AgentExceptionMethod.Comment && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`exception_default_value`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{t('flow.ExceptionDefaultValue')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormContainer>
|
||||||
|
</Collapse>
|
||||||
|
<Output list={outputList}></Output>
|
||||||
|
</FormWrapper>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(AgentForm);
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Operator } from '@/pages/agent/constant';
|
||||||
|
import { AgentFormContext, AgentInstanceContext } from '@/pages/agent/context';
|
||||||
|
import useGraphStore from '@/pages/agent/store';
|
||||||
|
import { Position } from '@xyflow/react';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { PropsWithChildren, useCallback, useContext, useEffect } from 'react';
|
||||||
|
import { useGetAgentMCPIds, useGetAgentToolNames } from '../use-get-tools';
|
||||||
|
import { MCPCommand, ToolCommand } from './tool-command';
|
||||||
|
import { useUpdateAgentNodeMCP } from './use-update-mcp';
|
||||||
|
import { useUpdateAgentNodeTools } from './use-update-tools';
|
||||||
|
|
||||||
|
enum ToolType {
|
||||||
|
Common = 'common',
|
||||||
|
MCP = 'mcp',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToolPopover({ children }: PropsWithChildren) {
|
||||||
|
const { addCanvasNode } = useContext(AgentInstanceContext);
|
||||||
|
const node = useContext(AgentFormContext);
|
||||||
|
const { updateNodeTools } = useUpdateAgentNodeTools();
|
||||||
|
const { toolNames } = useGetAgentToolNames();
|
||||||
|
const deleteAgentToolNodeById = useGraphStore(
|
||||||
|
(state) => state.deleteAgentToolNodeById,
|
||||||
|
);
|
||||||
|
const { mcpIds } = useGetAgentMCPIds();
|
||||||
|
const { updateNodeMCP } = useUpdateAgentNodeMCP();
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(value: string[]) => {
|
||||||
|
if (Array.isArray(value) && node?.id) {
|
||||||
|
updateNodeTools(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[node?.id, updateNodeTools],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const total = toolNames.length + mcpIds.length;
|
||||||
|
if (node?.id) {
|
||||||
|
if (total > 0) {
|
||||||
|
addCanvasNode(Operator.Tool, {
|
||||||
|
position: Position.Bottom,
|
||||||
|
nodeId: node?.id,
|
||||||
|
})();
|
||||||
|
} else {
|
||||||
|
deleteAgentToolNodeById(node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
addCanvasNode,
|
||||||
|
deleteAgentToolNodeById,
|
||||||
|
mcpIds.length,
|
||||||
|
node?.id,
|
||||||
|
toolNames.length,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-80 p-4">
|
||||||
|
<Tabs defaultValue={ToolType.Common}>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value={ToolType.Common} className="bg-bg-card">
|
||||||
|
{t('flow.builtIn')}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value={ToolType.MCP} className="bg-bg-card">
|
||||||
|
MCP
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value={ToolType.Common}>
|
||||||
|
<ToolCommand
|
||||||
|
onChange={handleChange}
|
||||||
|
value={toolNames}
|
||||||
|
></ToolCommand>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value={ToolType.MCP}>
|
||||||
|
<MCPCommand value={mcpIds} onChange={updateNodeMCP}></MCPCommand>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,178 @@
|
|||||||
|
import { CheckIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from '@/components/ui/command';
|
||||||
|
import { useListMcpServer } from '@/hooks/use-mcp-request';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Operator } from '@/pages/agent/constant';
|
||||||
|
import OperatorIcon from '@/pages/agent/operator-icon';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { lowerFirst } from 'lodash';
|
||||||
|
import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const Menus = [
|
||||||
|
{
|
||||||
|
label: t('flow.search'),
|
||||||
|
list: [
|
||||||
|
Operator.TavilySearch,
|
||||||
|
Operator.TavilyExtract,
|
||||||
|
Operator.Google,
|
||||||
|
// Operator.Bing,
|
||||||
|
Operator.DuckDuckGo,
|
||||||
|
Operator.Wikipedia,
|
||||||
|
Operator.SearXNG,
|
||||||
|
Operator.YahooFinance,
|
||||||
|
Operator.PubMed,
|
||||||
|
Operator.GoogleScholar,
|
||||||
|
Operator.ArXiv,
|
||||||
|
Operator.WenCai,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('flow.communication'),
|
||||||
|
list: [Operator.Email],
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// label: 'Productivity',
|
||||||
|
// list: [],
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
label: t('flow.developer'),
|
||||||
|
list: [Operator.GitHub, Operator.ExeSQL, Operator.Code, Operator.Retrieval],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type ToolCommandProps = {
|
||||||
|
value?: string[];
|
||||||
|
onChange?(values: string[]): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToolCommandItemProps = {
|
||||||
|
toggleOption(id: string): void;
|
||||||
|
id: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
} & ToolCommandProps;
|
||||||
|
|
||||||
|
function ToolCommandItem({
|
||||||
|
toggleOption,
|
||||||
|
id,
|
||||||
|
isSelected,
|
||||||
|
children,
|
||||||
|
}: ToolCommandItemProps & PropsWithChildren) {
|
||||||
|
return (
|
||||||
|
<CommandItem className="cursor-pointer" onSelect={() => toggleOption(id)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||||
|
isSelected
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'opacity-50 [&_svg]:invisible',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useHandleSelectChange({ onChange, value }: ToolCommandProps) {
|
||||||
|
const [currentValue, setCurrentValue] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const toggleOption = useCallback(
|
||||||
|
(option: string) => {
|
||||||
|
const newSelectedValues = currentValue.includes(option)
|
||||||
|
? currentValue.filter((value) => value !== option)
|
||||||
|
: [...currentValue, option];
|
||||||
|
setCurrentValue(newSelectedValues);
|
||||||
|
onChange?.(newSelectedValues);
|
||||||
|
},
|
||||||
|
[currentValue, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
setCurrentValue(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
toggleOption,
|
||||||
|
currentValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToolCommand({ value, onChange }: ToolCommandProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { toggleOption, currentValue } = useHandleSelectChange({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder={t('flow.typeCommandOrsearch')} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
{Menus.map((x) => (
|
||||||
|
<CommandGroup heading={x.label} key={x.label}>
|
||||||
|
{x.list.map((y) => {
|
||||||
|
const isSelected = currentValue.includes(y);
|
||||||
|
return (
|
||||||
|
<ToolCommandItem
|
||||||
|
key={y}
|
||||||
|
id={y}
|
||||||
|
toggleOption={toggleOption}
|
||||||
|
isSelected={isSelected}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<OperatorIcon name={y as Operator}></OperatorIcon>
|
||||||
|
<span>{t(`flow.${lowerFirst(y)}`)}</span>
|
||||||
|
</>
|
||||||
|
</ToolCommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
))}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MCPCommand({ onChange, value }: ToolCommandProps) {
|
||||||
|
const { data } = useListMcpServer();
|
||||||
|
const { toggleOption, currentValue } = useHandleSelectChange({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Type a command or search..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
{data.mcp_servers.map((item) => {
|
||||||
|
const isSelected = currentValue.includes(item.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolCommandItem
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
toggleOption={toggleOption}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</ToolCommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { useListMcpServer } from '@/hooks/use-mcp-request';
|
||||||
|
import { IAgentForm } from '@/interfaces/database/agent';
|
||||||
|
import { AgentFormContext } from '@/pages/agent/context';
|
||||||
|
import useGraphStore from '@/pages/agent/store';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { useCallback, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
export function useGetNodeMCP() {
|
||||||
|
const node = useContext(AgentFormContext);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const mcp: IAgentForm['mcp'] = get(node, 'data.form.mcp');
|
||||||
|
return mcp;
|
||||||
|
}, [node]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateAgentNodeMCP() {
|
||||||
|
const { updateNodeForm } = useGraphStore((state) => state);
|
||||||
|
const node = useContext(AgentFormContext);
|
||||||
|
const mcpList = useGetNodeMCP();
|
||||||
|
const { data } = useListMcpServer();
|
||||||
|
const mcpServers = data.mcp_servers;
|
||||||
|
|
||||||
|
const findMcpTools = useCallback(
|
||||||
|
(mcpId: string) => {
|
||||||
|
const mcp = mcpServers.find((x) => x.id === mcpId);
|
||||||
|
return mcp?.variables.tools;
|
||||||
|
},
|
||||||
|
[mcpServers],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateNodeMCP = useCallback(
|
||||||
|
(value: string[]) => {
|
||||||
|
if (node?.id) {
|
||||||
|
const nextValue = value.reduce<IAgentForm['mcp']>((pre, cur) => {
|
||||||
|
const mcp = mcpList.find((x) => x.mcp_id === cur);
|
||||||
|
const tools = findMcpTools(cur);
|
||||||
|
if (mcp) {
|
||||||
|
pre.push(mcp);
|
||||||
|
} else if (tools) {
|
||||||
|
pre.push({
|
||||||
|
mcp_id: cur,
|
||||||
|
tools: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pre;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
updateNodeForm(node?.id, nextValue, ['mcp']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[node?.id, updateNodeForm, mcpList, findMcpTools],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { updateNodeMCP };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteAgentNodeMCP() {
|
||||||
|
const { updateNodeForm } = useGraphStore((state) => state);
|
||||||
|
const mcpList = useGetNodeMCP();
|
||||||
|
const node = useContext(AgentFormContext);
|
||||||
|
|
||||||
|
const deleteNodeMCP = useCallback(
|
||||||
|
(value: string) => () => {
|
||||||
|
const nextMCP = mcpList.filter((x) => x.mcp_id !== value);
|
||||||
|
if (node?.id) {
|
||||||
|
updateNodeForm(node?.id, nextMCP, ['mcp']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[node?.id, mcpList, updateNodeForm],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { deleteNodeMCP };
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { IAgentForm } from '@/interfaces/database/agent';
|
||||||
|
import { Operator } from '@/pages/agent/constant';
|
||||||
|
import { AgentFormContext } from '@/pages/agent/context';
|
||||||
|
import { useAgentToolInitialValues } from '@/pages/agent/hooks/use-agent-tool-initial-values';
|
||||||
|
import useGraphStore from '@/pages/agent/store';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { useCallback, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
export function useGetNodeTools() {
|
||||||
|
const node = useContext(AgentFormContext);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const tools: IAgentForm['tools'] = get(node, 'data.form.tools');
|
||||||
|
return tools;
|
||||||
|
}, [node]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateAgentNodeTools() {
|
||||||
|
const { updateNodeForm } = useGraphStore((state) => state);
|
||||||
|
const node = useContext(AgentFormContext);
|
||||||
|
const tools = useGetNodeTools();
|
||||||
|
const { initializeAgentToolValues } = useAgentToolInitialValues();
|
||||||
|
|
||||||
|
const updateNodeTools = useCallback(
|
||||||
|
(value: string[]) => {
|
||||||
|
if (node?.id) {
|
||||||
|
const nextValue = value.reduce<IAgentForm['tools']>((pre, cur) => {
|
||||||
|
const tool = tools.find((x) => x.component_name === cur);
|
||||||
|
pre.push(
|
||||||
|
tool
|
||||||
|
? tool
|
||||||
|
: {
|
||||||
|
component_name: cur,
|
||||||
|
name: cur,
|
||||||
|
params: initializeAgentToolValues(cur as Operator),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return pre;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
updateNodeForm(node?.id, nextValue, ['tools']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[initializeAgentToolValues, node?.id, tools, updateNodeForm],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { updateNodeTools };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteAgentNodeTools() {
|
||||||
|
const { updateNodeForm } = useGraphStore((state) => state);
|
||||||
|
const tools = useGetNodeTools();
|
||||||
|
const node = useContext(AgentFormContext);
|
||||||
|
|
||||||
|
const deleteNodeTool = useCallback(
|
||||||
|
(value: string) => () => {
|
||||||
|
const nextTools = tools.filter((x) => x.component_name !== value);
|
||||||
|
if (node?.id) {
|
||||||
|
updateNodeForm(node?.id, nextTools, ['tools']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[node?.id, tools, updateNodeForm],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { deleteNodeTool };
|
||||||
|
}
|
||||||
26
web/src/pages/data-flow/form/agent-form/use-get-tools.ts
Normal file
26
web/src/pages/data-flow/form/agent-form/use-get-tools.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { IAgentForm } from '@/interfaces/database/agent';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { useContext, useMemo } from 'react';
|
||||||
|
import { AgentFormContext } from '../../context';
|
||||||
|
|
||||||
|
export function useGetAgentToolNames() {
|
||||||
|
const node = useContext(AgentFormContext);
|
||||||
|
|
||||||
|
const toolNames = useMemo(() => {
|
||||||
|
const tools: IAgentForm['tools'] = get(node, 'data.form.tools', []);
|
||||||
|
return tools.map((x) => x.component_name);
|
||||||
|
}, [node]);
|
||||||
|
|
||||||
|
return { toolNames };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetAgentMCPIds() {
|
||||||
|
const node = useContext(AgentFormContext);
|
||||||
|
|
||||||
|
const mcpIds = useMemo(() => {
|
||||||
|
const ids: IAgentForm['mcp'] = get(node, 'data.form.mcp', []);
|
||||||
|
return ids.map((x) => x.mcp_id);
|
||||||
|
}, [node]);
|
||||||
|
|
||||||
|
return { mcpIds };
|
||||||
|
}
|
||||||
33
web/src/pages/data-flow/form/agent-form/use-values.ts
Normal file
33
web/src/pages/data-flow/form/agent-form/use-values.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useFetchModelId } from '@/hooks/logic-hooks';
|
||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { get, isEmpty } from 'lodash';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { initialAgentValues } from '../../constant';
|
||||||
|
|
||||||
|
export function useValues(node?: RAGFlowNodeType) {
|
||||||
|
const llmId = useFetchModelId();
|
||||||
|
|
||||||
|
const defaultValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
...initialAgentValues,
|
||||||
|
llm_id: llmId,
|
||||||
|
prompts: '',
|
||||||
|
}),
|
||||||
|
[llmId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const values = useMemo(() => {
|
||||||
|
const formData = node?.data?.form;
|
||||||
|
|
||||||
|
if (isEmpty(formData)) {
|
||||||
|
return defaultValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...formData,
|
||||||
|
prompts: get(formData, 'prompts.0.content', ''),
|
||||||
|
};
|
||||||
|
}, [defaultValues, node?.data?.form]);
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
22
web/src/pages/data-flow/form/agent-form/use-watch-change.ts
Normal file
22
web/src/pages/data-flow/form/agent-form/use-watch-change.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||||
|
import { PromptRole } from '../../constant';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
|
||||||
|
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
|
||||||
|
let values = useWatch({ control: form?.control });
|
||||||
|
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Manually triggered form updates are synchronized to the canvas
|
||||||
|
if (id && form?.formState.isDirty) {
|
||||||
|
values = form?.getValues();
|
||||||
|
let nextValues: any = {
|
||||||
|
...values,
|
||||||
|
prompts: [{ role: PromptRole.User, content: values.prompts }],
|
||||||
|
};
|
||||||
|
|
||||||
|
updateNodeForm(id, nextValues);
|
||||||
|
}
|
||||||
|
}, [form?.formState.isDirty, id, updateNodeForm, values]);
|
||||||
|
}
|
||||||
22
web/src/pages/data-flow/form/akshare-form/index.tsx
Normal file
22
web/src/pages/data-flow/form/akshare-form/index.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { TopNFormField } from '@/components/top-n-item';
|
||||||
|
import { Form } from '@/components/ui/form';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
|
||||||
|
|
||||||
|
const AkShareForm = ({ form, node }: INextOperatorForm) => {
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="space-y-6"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||||
|
<TopNFormField max={99}></TopNFormField>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AkShareForm;
|
||||||
96
web/src/pages/data-flow/form/arxiv-form/index.tsx
Normal file
96
web/src/pages/data-flow/form/arxiv-form/index.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { FormContainer } from '@/components/form-container';
|
||||||
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import { TopNFormField } from '@/components/top-n-item';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useForm, useFormContext } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { initialArXivValues } 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';
|
||||||
|
import { QueryVariable } from '../components/query-variable';
|
||||||
|
|
||||||
|
export const ArXivFormPartialSchema = {
|
||||||
|
top_n: z.number(),
|
||||||
|
sort_by: z.string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormSchema = z.object({
|
||||||
|
...ArXivFormPartialSchema,
|
||||||
|
query: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ArXivFormWidgets() {
|
||||||
|
const form = useFormContext();
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return ['submittedDate', 'lastUpdatedDate', 'relevance'].map((x) => ({
|
||||||
|
value: x,
|
||||||
|
label: t(x),
|
||||||
|
}));
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TopNFormField></TopNFormField>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`sort_by`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{t('sortBy')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SelectWithSearch {...field} options={options}></SelectWithSearch>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputList = buildOutputList(initialArXivValues.outputs);
|
||||||
|
|
||||||
|
function ArXivForm({ node }: INextOperatorForm) {
|
||||||
|
const defaultValues = useFormValues(initialArXivValues, node);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
defaultValues,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<FormWrapper>
|
||||||
|
<FormContainer>
|
||||||
|
<QueryVariable></QueryVariable>
|
||||||
|
</FormContainer>
|
||||||
|
<FormContainer>
|
||||||
|
<ArXivFormWidgets></ArXivFormWidgets>
|
||||||
|
</FormContainer>
|
||||||
|
</FormWrapper>
|
||||||
|
<div className="p-5">
|
||||||
|
<Output list={outputList}></Output>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ArXivForm);
|
||||||
71
web/src/pages/data-flow/form/baidu-fanyi-form/index.tsx
Normal file
71
web/src/pages/data-flow/form/baidu-fanyi-form/index.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { Form, Input, Select } from 'antd';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { IOperatorForm } from '../../interface';
|
||||||
|
import {
|
||||||
|
BaiduFanyiDomainOptions,
|
||||||
|
BaiduFanyiSourceLangOptions,
|
||||||
|
} from '../../options';
|
||||||
|
import DynamicInputVariable from '../components/dynamic-input-variable';
|
||||||
|
|
||||||
|
const BaiduFanyiForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return ['translate', 'fieldtranslate'].map((x) => ({
|
||||||
|
value: x,
|
||||||
|
label: t(`baiduSecretKeyOptions.${x}`),
|
||||||
|
}));
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const baiduFanyiOptions = useMemo(() => {
|
||||||
|
return BaiduFanyiDomainOptions.map((x) => ({
|
||||||
|
value: x,
|
||||||
|
label: t(`baiduDomainOptions.${x}`),
|
||||||
|
}));
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const baiduFanyiSourceLangOptions = useMemo(() => {
|
||||||
|
return BaiduFanyiSourceLangOptions.map((x) => ({
|
||||||
|
value: x,
|
||||||
|
label: t(`baiduSourceLangOptions.${x}`),
|
||||||
|
}));
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
name="basic"
|
||||||
|
autoComplete="off"
|
||||||
|
form={form}
|
||||||
|
onValuesChange={onValuesChange}
|
||||||
|
layout={'vertical'}
|
||||||
|
>
|
||||||
|
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||||
|
<Form.Item label={t('appid')} name={'appid'}>
|
||||||
|
<Input></Input>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('secretKey')} name={'secret_key'}>
|
||||||
|
<Input></Input>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('transType')} name={'trans_type'}>
|
||||||
|
<Select options={options}></Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item noStyle dependencies={['model_type']}>
|
||||||
|
{({ getFieldValue }) =>
|
||||||
|
getFieldValue('trans_type') === 'fieldtranslate' && (
|
||||||
|
<Form.Item label={t('domain')} name={'domain'}>
|
||||||
|
<Select options={baiduFanyiOptions}></Select>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('sourceLang')} name={'source_lang'}>
|
||||||
|
<Select options={baiduFanyiSourceLangOptions}></Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('targetLang')} name={'target_lang'}>
|
||||||
|
<Select options={baiduFanyiSourceLangOptions}></Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaiduFanyiForm;
|
||||||
22
web/src/pages/data-flow/form/baidu-form/index.tsx
Normal file
22
web/src/pages/data-flow/form/baidu-form/index.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { TopNFormField } from '@/components/top-n-item';
|
||||||
|
import { Form } from '@/components/ui/form';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
|
||||||
|
|
||||||
|
const BaiduForm = ({ form, node }: INextOperatorForm) => {
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="space-y-6"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||||
|
<TopNFormField></TopNFormField>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaiduForm;
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { BlockButton, Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function BeginDynamicOptions() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext();
|
||||||
|
const name = 'options';
|
||||||
|
|
||||||
|
const { fields, remove, append } = useFieldArray({
|
||||||
|
name: name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-5">
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const typeField = `${name}.${index}.value`;
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="flex items-center gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={typeField}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={t('common.pleaseInput')}
|
||||||
|
></Input>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||||
|
<X className="text-text-sub-title-invert " />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<BlockButton onClick={() => append({ value: '' })} type="button">
|
||||||
|
{t('flow.addField')}
|
||||||
|
</BlockButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
205
web/src/pages/data-flow/form/begin-form/index.tsx
Normal file
205
web/src/pages/data-flow/form/begin-form/index.tsx
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import { Collapse } from '@/components/collapse';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { RAGFlowSelect } from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { FormTooltip } from '@/components/ui/tooltip';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { memo, useEffect, useRef } from 'react';
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { AgentDialogueMode } from '../../constant';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { ParameterDialog } from './parameter-dialog';
|
||||||
|
import { QueryTable } from './query-table';
|
||||||
|
import { useEditQueryRecord } from './use-edit-query';
|
||||||
|
import { useValues } from './use-values';
|
||||||
|
import { useWatchFormChange } from './use-watch-change';
|
||||||
|
|
||||||
|
const ModeOptions = [
|
||||||
|
{ value: AgentDialogueMode.Conversational, label: t('flow.conversational') },
|
||||||
|
{ value: AgentDialogueMode.Task, label: t('flow.task') },
|
||||||
|
];
|
||||||
|
|
||||||
|
function BeginForm({ node }: INextOperatorForm) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const values = useValues(node);
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
enablePrologue: z.boolean().optional(),
|
||||||
|
prologue: z.string().trim().optional(),
|
||||||
|
mode: z.string(),
|
||||||
|
inputs: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
key: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
optional: z.boolean(),
|
||||||
|
name: z.string(),
|
||||||
|
options: z.array(z.union([z.number(), z.string(), z.boolean()])),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: values,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
const inputs = useWatch({ control: form.control, name: 'inputs' });
|
||||||
|
const mode = useWatch({ control: form.control, name: 'mode' });
|
||||||
|
|
||||||
|
const enablePrologue = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: 'enablePrologue',
|
||||||
|
});
|
||||||
|
|
||||||
|
const previousModeRef = useRef(mode);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
previousModeRef.current === AgentDialogueMode.Task &&
|
||||||
|
mode === AgentDialogueMode.Conversational
|
||||||
|
) {
|
||||||
|
form.setValue('enablePrologue', true);
|
||||||
|
}
|
||||||
|
previousModeRef.current = mode;
|
||||||
|
}, [mode, form]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ok,
|
||||||
|
currentRecord,
|
||||||
|
visible,
|
||||||
|
hideModal,
|
||||||
|
showModal,
|
||||||
|
otherThanCurrentQuery,
|
||||||
|
handleDeleteRecord,
|
||||||
|
} = useEditQueryRecord({
|
||||||
|
form,
|
||||||
|
node,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="px-5 space-y-5">
|
||||||
|
<Form {...form}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={'mode'}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel tooltip={t('flow.modeTip')}>
|
||||||
|
{t('flow.mode')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RAGFlowSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={ModeOptions}
|
||||||
|
{...field}
|
||||||
|
></RAGFlowSelect>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{mode === AgentDialogueMode.Conversational && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={'enablePrologue'}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel tooltip={t('flow.openingSwitchTip')}>
|
||||||
|
{t('flow.openingSwitch')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{mode === AgentDialogueMode.Conversational && enablePrologue && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={'prologue'}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel tooltip={t('chat.setAnOpenerTip')}>
|
||||||
|
{t('flow.openingCopy')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
rows={5}
|
||||||
|
{...field}
|
||||||
|
placeholder={t('common.pleaseInput')}
|
||||||
|
></Textarea>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Create a hidden field to make Form instance record this */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={'inputs'}
|
||||||
|
render={() => <div></div>}
|
||||||
|
/>
|
||||||
|
<Collapse
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
{t('flow.input')}
|
||||||
|
<FormTooltip tooltip={t('flow.beginInputTip')}></FormTooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightContent={
|
||||||
|
<Button
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<QueryTable
|
||||||
|
data={inputs}
|
||||||
|
showModal={showModal}
|
||||||
|
deleteRecord={handleDeleteRecord}
|
||||||
|
></QueryTable>
|
||||||
|
</Collapse>
|
||||||
|
{visible && (
|
||||||
|
<ParameterDialog
|
||||||
|
hideModal={hideModal}
|
||||||
|
initialValue={currentRecord}
|
||||||
|
otherThanCurrentQuery={otherThanCurrentQuery}
|
||||||
|
submit={ok}
|
||||||
|
></ParameterDialog>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(BeginForm);
|
||||||
226
web/src/pages/data-flow/form/begin-form/parameter-dialog.tsx
Normal file
226
web/src/pages/data-flow/form/begin-form/parameter-dialog.tsx
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { ChangeEvent, useEffect, useMemo } from 'react';
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant';
|
||||||
|
import { BeginQuery } from '../../interface';
|
||||||
|
import { BeginDynamicOptions } from './begin-dynamic-options';
|
||||||
|
|
||||||
|
type ModalFormProps = {
|
||||||
|
initialValue: BeginQuery;
|
||||||
|
otherThanCurrentQuery: BeginQuery[];
|
||||||
|
submit(values: any): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormId = 'BeginParameterForm';
|
||||||
|
|
||||||
|
function ParameterForm({
|
||||||
|
initialValue,
|
||||||
|
otherThanCurrentQuery,
|
||||||
|
submit,
|
||||||
|
}: ModalFormProps) {
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
const FormSchema = z.object({
|
||||||
|
type: z.string(),
|
||||||
|
key: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine(
|
||||||
|
(value) =>
|
||||||
|
!value || !otherThanCurrentQuery.some((x) => x.key === value),
|
||||||
|
{ message: 'The key cannot be repeated!' },
|
||||||
|
),
|
||||||
|
optional: z.boolean(),
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
options: z
|
||||||
|
.array(z.object({ value: z.string().or(z.boolean()).or(z.number()) }))
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues: {
|
||||||
|
type: BeginQueryType.Line,
|
||||||
|
optional: false,
|
||||||
|
key: '',
|
||||||
|
name: '',
|
||||||
|
options: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return Object.values(BeginQueryType).reduce<RAGFlowSelectOptionType[]>(
|
||||||
|
(pre, cur) => {
|
||||||
|
const Icon = BeginQueryTypeIconMap[cur];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...pre,
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Icon
|
||||||
|
className={`size-${cur === BeginQueryType.Options ? 4 : 5}`}
|
||||||
|
></Icon>
|
||||||
|
{t(cur.toLowerCase())}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: cur,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const type = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: 'type',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEmpty(initialValue)) {
|
||||||
|
form.reset({
|
||||||
|
...initialValue,
|
||||||
|
options: initialValue.options?.map((x) => ({ value: x })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, initialValue]);
|
||||||
|
|
||||||
|
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||||
|
const values = { ...data, options: data.options?.map((x) => x.value) };
|
||||||
|
console.log('🚀 ~ onSubmit ~ values:', values);
|
||||||
|
|
||||||
|
submit(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const name = form.getValues().name || '';
|
||||||
|
form.setValue('key', e.target.value.trim());
|
||||||
|
if (!name) {
|
||||||
|
form.setValue('name', e.target.value.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
id={FormId}
|
||||||
|
className="space-y-5"
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
name="type"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('type')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RAGFlowSelect {...field} options={options} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name="key"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('key')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} autoComplete="off" onBlur={handleKeyChange} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name="name"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('name')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name="optional"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('optional')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{type === BeginQueryType.Options && (
|
||||||
|
<BeginDynamicOptions></BeginDynamicOptions>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ParameterDialog({
|
||||||
|
initialValue,
|
||||||
|
hideModal,
|
||||||
|
otherThanCurrentQuery,
|
||||||
|
submit,
|
||||||
|
}: ModalFormProps & IModalProps<BeginQuery>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open onOpenChange={hideModal}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('flow.variableSettings')}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<ParameterForm
|
||||||
|
initialValue={initialValue}
|
||||||
|
otherThanCurrentQuery={otherThanCurrentQuery}
|
||||||
|
submit={submit}
|
||||||
|
></ParameterForm>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" form={FormId}>
|
||||||
|
{t('modal.okText')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
199
web/src/pages/data-flow/form/begin-form/query-table.tsx
Normal file
199
web/src/pages/data-flow/form/begin-form/query-table.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
ColumnFiltersState,
|
||||||
|
SortingState,
|
||||||
|
VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { TableEmpty } from '@/components/table-skeleton';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { BeginQuery } from '../../interface';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
data: BeginQuery[];
|
||||||
|
deleteRecord(index: number): void;
|
||||||
|
showModal(index: number, record: BeginQuery): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QueryTable({ data = [], deleteRecord, showModal }: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [columnVisibility, setColumnVisibility] =
|
||||||
|
React.useState<VisibilityState>({});
|
||||||
|
|
||||||
|
const columns: ColumnDef<BeginQuery>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'key',
|
||||||
|
header: t('flow.key'),
|
||||||
|
meta: { cellClassName: 'max-w-30' },
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const key: string = row.getValue('key');
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="truncate ">{key}</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{key}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: t('flow.name'),
|
||||||
|
meta: { cellClassName: 'max-w-30' },
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const name: string = row.getValue('name');
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="truncate">{name}</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{name}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'type',
|
||||||
|
header: t('flow.type'),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
{t(`flow.${(row.getValue('type')?.toString() || '').toLowerCase()}`)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'optional',
|
||||||
|
header: t('flow.optional'),
|
||||||
|
cell: ({ row }) => <div>{row.getValue('optional') ? 'Yes' : 'No'}</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
enableHiding: false,
|
||||||
|
header: t('common.action'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const record = row.original;
|
||||||
|
const idx = row.index;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => showModal(idx, record)}
|
||||||
|
>
|
||||||
|
<Pencil />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => deleteRecord(idx)}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnFilters,
|
||||||
|
columnVisibility,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table rootClassName="rounded-md">
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && 'selected'}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
key={cell.id}
|
||||||
|
className={cn(cell.column.columnDef.meta?.cellClassName)}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableEmpty columnsLength={columns.length}></TableEmpty>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
web/src/pages/data-flow/form/begin-form/use-edit-query.ts
Normal file
67
web/src/pages/data-flow/form/begin-form/use-edit-query.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
|
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||||
|
import { BeginQuery, INextOperatorForm } from '../../interface';
|
||||||
|
|
||||||
|
export const useEditQueryRecord = ({
|
||||||
|
form,
|
||||||
|
}: INextOperatorForm & { form: UseFormReturn }) => {
|
||||||
|
const { setRecord, currentRecord } = useSetSelectedRecord<BeginQuery>();
|
||||||
|
const { visible, hideModal, showModal } = useSetModalState();
|
||||||
|
const [index, setIndex] = useState(-1);
|
||||||
|
const inputs: BeginQuery[] = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: 'inputs',
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherThanCurrentQuery = useMemo(() => {
|
||||||
|
return inputs.filter((item, idx) => idx !== index);
|
||||||
|
}, [index, inputs]);
|
||||||
|
|
||||||
|
const handleEditRecord = useCallback(
|
||||||
|
(record: BeginQuery) => {
|
||||||
|
const inputs: BeginQuery[] = form?.getValues('inputs') || [];
|
||||||
|
|
||||||
|
const nextQuery: BeginQuery[] =
|
||||||
|
index > -1 ? inputs.toSpliced(index, 1, record) : [...inputs, record];
|
||||||
|
|
||||||
|
form.setValue('inputs', nextQuery);
|
||||||
|
|
||||||
|
hideModal();
|
||||||
|
},
|
||||||
|
[form, hideModal, index],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleShowModal = useCallback(
|
||||||
|
(idx?: number, record?: BeginQuery) => {
|
||||||
|
setIndex(idx ?? -1);
|
||||||
|
setRecord(record ?? ({} as BeginQuery));
|
||||||
|
showModal();
|
||||||
|
},
|
||||||
|
[setRecord, showModal],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteRecord = useCallback(
|
||||||
|
(idx: number) => {
|
||||||
|
const inputs = form?.getValues('inputs') || [];
|
||||||
|
const nextInputs = inputs.filter(
|
||||||
|
(item: BeginQuery, index: number) => index !== idx,
|
||||||
|
);
|
||||||
|
|
||||||
|
form.setValue('inputs', nextInputs);
|
||||||
|
},
|
||||||
|
[form],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: handleEditRecord,
|
||||||
|
currentRecord,
|
||||||
|
setRecord,
|
||||||
|
visible,
|
||||||
|
hideModal,
|
||||||
|
showModal: handleShowModal,
|
||||||
|
otherThanCurrentQuery,
|
||||||
|
handleDeleteRecord,
|
||||||
|
};
|
||||||
|
};
|
||||||
34
web/src/pages/data-flow/form/begin-form/use-values.ts
Normal file
34
web/src/pages/data-flow/form/begin-form/use-values.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AgentDialogueMode } from '../../constant';
|
||||||
|
import { buildBeginInputListFromObject } from './utils';
|
||||||
|
|
||||||
|
export function useValues(node?: RAGFlowNodeType) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const defaultValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
enablePrologue: true,
|
||||||
|
prologue: t('chat.setAnOpenerInitial'),
|
||||||
|
mode: AgentDialogueMode.Conversational,
|
||||||
|
inputs: [],
|
||||||
|
}),
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const values = useMemo(() => {
|
||||||
|
const formData = node?.data?.form;
|
||||||
|
|
||||||
|
if (isEmpty(formData)) {
|
||||||
|
return defaultValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = buildBeginInputListFromObject(formData?.inputs);
|
||||||
|
|
||||||
|
return { ...(formData || {}), inputs };
|
||||||
|
}, [defaultValues, node?.data?.form]);
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
31
web/src/pages/data-flow/form/begin-form/use-watch-change.ts
Normal file
31
web/src/pages/data-flow/form/begin-form/use-watch-change.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { omit } from 'lodash';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||||
|
import { BeginQuery } from '../../interface';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
|
||||||
|
export function transferInputsArrayToObject(inputs: BeginQuery[] = []) {
|
||||||
|
return inputs.reduce<Record<string, Omit<BeginQuery, 'key'>>>((pre, cur) => {
|
||||||
|
pre[cur.key] = omit(cur, 'key');
|
||||||
|
|
||||||
|
return pre;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWatchFormChange(id?: string, form?: UseFormReturn) {
|
||||||
|
let values = useWatch({ control: form?.control });
|
||||||
|
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
values = form?.getValues() || {};
|
||||||
|
|
||||||
|
const nextValues = {
|
||||||
|
...values,
|
||||||
|
inputs: transferInputsArrayToObject(values.inputs),
|
||||||
|
};
|
||||||
|
|
||||||
|
updateNodeForm(id, nextValues);
|
||||||
|
}
|
||||||
|
}, [form?.formState.isDirty, id, updateNodeForm, values]);
|
||||||
|
}
|
||||||
14
web/src/pages/data-flow/form/begin-form/utils.ts
Normal file
14
web/src/pages/data-flow/form/begin-form/utils.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { BeginQuery } from '../../interface';
|
||||||
|
|
||||||
|
export function buildBeginInputListFromObject(
|
||||||
|
inputs: Record<string, Omit<BeginQuery, 'key'>>,
|
||||||
|
) {
|
||||||
|
return Object.entries(inputs || {}).reduce<BeginQuery[]>(
|
||||||
|
(pre, [key, value]) => {
|
||||||
|
pre.push({ ...(value || {}), key });
|
||||||
|
|
||||||
|
return pre;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
131
web/src/pages/data-flow/form/bing-form/index.tsx
Normal file
131
web/src/pages/data-flow/form/bing-form/index.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import { TopNFormField } from '@/components/top-n-item';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useForm, useFormContext } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { initialBingValues } from '../../constant';
|
||||||
|
import { useFormValues } from '../../hooks/use-form-values';
|
||||||
|
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { BingCountryOptions, BingLanguageOptions } from '../../options';
|
||||||
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
|
import { QueryVariable } from '../components/query-variable';
|
||||||
|
|
||||||
|
export const BingFormSchema = {
|
||||||
|
channel: z.string(),
|
||||||
|
api_key: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
language: z.string(),
|
||||||
|
top_n: z.number(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormSchema = z.object({
|
||||||
|
query: z.string().optional(),
|
||||||
|
...BingFormSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function BingFormWidgets() {
|
||||||
|
const form = useFormContext();
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return ['Webpages', 'News'].map((x) => ({ label: x, value: x }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TopNFormField></TopNFormField>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="channel"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('channel')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SelectWithSearch {...field} options={options}></SelectWithSearch>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('apiKey')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field}></Input>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="country"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('country')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SelectWithSearch
|
||||||
|
{...field}
|
||||||
|
options={BingCountryOptions}
|
||||||
|
></SelectWithSearch>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="language"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('language')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<SelectWithSearch
|
||||||
|
{...field}
|
||||||
|
options={BingLanguageOptions}
|
||||||
|
></SelectWithSearch>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BingForm({ node }: INextOperatorForm) {
|
||||||
|
const defaultValues = useFormValues(initialBingValues, node);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<FormWrapper>
|
||||||
|
<QueryVariable></QueryVariable>
|
||||||
|
<BingFormWidgets></BingFormWidgets>
|
||||||
|
</FormWrapper>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(BingForm);
|
||||||
@ -0,0 +1,249 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { BlurTextarea } from '@/components/ui/textarea';
|
||||||
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { useUpdateNodeInternals } from '@xyflow/react';
|
||||||
|
import humanId from 'human-id';
|
||||||
|
import trim from 'lodash/trim';
|
||||||
|
import { ChevronsUpDown, X } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
FocusEventHandler,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { UseFormReturn, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
import DynamicExample from './dynamic-example';
|
||||||
|
import { useCreateCategorizeFormSchema } from './use-form-schema';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
nodeId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface INameInputProps {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
otherNames?: string[];
|
||||||
|
validate(error?: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOtherFieldValues = (
|
||||||
|
form: UseFormReturn,
|
||||||
|
formListName: string = 'items',
|
||||||
|
index: number,
|
||||||
|
latestField: string,
|
||||||
|
) =>
|
||||||
|
(form.getValues(formListName) ?? [])
|
||||||
|
.map((x: any) => x[latestField])
|
||||||
|
.filter(
|
||||||
|
(x: string) =>
|
||||||
|
x !== form.getValues(`${formListName}.${index}.${latestField}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
const InnerNameInput = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
otherNames,
|
||||||
|
validate,
|
||||||
|
}: INameInputProps) => {
|
||||||
|
const [name, setName] = useState<string | undefined>();
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
|
||||||
|
const handleNameChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setName(val);
|
||||||
|
const trimmedVal = trim(val);
|
||||||
|
// trigger validation
|
||||||
|
if (otherNames?.some((x) => x === trimmedVal)) {
|
||||||
|
validate(t('nameRepeatedMsg'));
|
||||||
|
} else if (trimmedVal === '') {
|
||||||
|
validate(t('nameRequiredMsg'));
|
||||||
|
} else {
|
||||||
|
validate('');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[otherNames, validate, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNameBlur: FocusEventHandler<HTMLInputElement> = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (otherNames?.every((x) => x !== val) && trim(val) !== '') {
|
||||||
|
onChange?.(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange, otherNames],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setName(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
onBlur={handleNameBlur}
|
||||||
|
></Input>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NameInput = memo(InnerNameInput);
|
||||||
|
|
||||||
|
const InnerFormSet = ({ index }: IProps & { index: number }) => {
|
||||||
|
const form = useFormContext();
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
|
||||||
|
const buildFieldName = useCallback(
|
||||||
|
(name: string) => {
|
||||||
|
return `items.${index}.${name}`;
|
||||||
|
},
|
||||||
|
[index],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={buildFieldName('name')}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('categoryName')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NameInput
|
||||||
|
{...field}
|
||||||
|
otherNames={getOtherFieldValues(form, 'items', index, 'name')}
|
||||||
|
validate={(error?: string) => {
|
||||||
|
const fieldName = buildFieldName('name');
|
||||||
|
if (error) {
|
||||||
|
form.setError(fieldName, { message: error });
|
||||||
|
} else {
|
||||||
|
form.clearErrors(fieldName);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></NameInput>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={buildFieldName('description')}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('description')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<BlurTextarea {...field} rows={3} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* Create a hidden field to make Form instance record this */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={'uuid'}
|
||||||
|
render={() => <div></div>}
|
||||||
|
/>
|
||||||
|
<DynamicExample name={buildFieldName('examples')}></DynamicExample>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormSet = memo(InnerFormSet);
|
||||||
|
|
||||||
|
const DynamicCategorize = ({ nodeId }: IProps) => {
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
const FormSchema = useCreateCategorizeFormSchema();
|
||||||
|
|
||||||
|
const deleteCategorizeCaseEdges = useGraphStore(
|
||||||
|
(state) => state.deleteEdgesBySourceAndSourceHandle,
|
||||||
|
);
|
||||||
|
const form = useFormContext<z.infer<typeof FormSchema>>();
|
||||||
|
const { t } = useTranslate('flow');
|
||||||
|
const { fields, remove, append } = useFieldArray({
|
||||||
|
name: 'items',
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdd = useCallback(() => {
|
||||||
|
append({
|
||||||
|
name: humanId(),
|
||||||
|
description: '',
|
||||||
|
uuid: uuid(),
|
||||||
|
examples: [{ value: '' }],
|
||||||
|
});
|
||||||
|
if (nodeId) updateNodeInternals(nodeId);
|
||||||
|
}, [append, nodeId, updateNodeInternals]);
|
||||||
|
|
||||||
|
const handleRemove = useCallback(
|
||||||
|
(index: number) => () => {
|
||||||
|
remove(index);
|
||||||
|
if (nodeId) {
|
||||||
|
const uuid = fields[index].uuid;
|
||||||
|
deleteCategorizeCaseEdges(nodeId, uuid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[deleteCategorizeCaseEdges, fields, nodeId, remove],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 ">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Collapsible key={field.id} defaultOpen>
|
||||||
|
<div className="flex items-center justify-between space-x-4">
|
||||||
|
<h4 className="font-bold">
|
||||||
|
{form.getValues(`items.${index}.name`)}
|
||||||
|
</h4>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-9 p-0"
|
||||||
|
onClick={handleRemove(index)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" className="w-9 p-0">
|
||||||
|
<ChevronsUpDown className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Toggle</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</div>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<FormSet nodeId={nodeId} index={index}></FormSet>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button type={'button'} onClick={handleAdd}>
|
||||||
|
<PlusOutlined />
|
||||||
|
{t('addCategory')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(DynamicCategorize);
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Plus, X } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type DynamicExampleProps = { name: string };
|
||||||
|
|
||||||
|
const DynamicExample = ({ name }: DynamicExampleProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext();
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.examples')}</FormLabel>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex items-start gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`${name}.${index}.value`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<Textarea {...field}> </Textarea>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{index === 0 ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => append({ value: '' })}
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(DynamicExample);
|
||||||
48
web/src/pages/data-flow/form/categorize-form/index.tsx
Normal file
48
web/src/pages/data-flow/form/categorize-form/index.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { FormContainer } from '@/components/form-container';
|
||||||
|
import { LargeModelFormField } from '@/components/large-model-form-field';
|
||||||
|
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
|
||||||
|
import { Form } from '@/components/ui/form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { initialCategorizeValues } from '../../constant';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
|
import { Output } from '../components/output';
|
||||||
|
import { QueryVariable } from '../components/query-variable';
|
||||||
|
import DynamicCategorize from './dynamic-categorize';
|
||||||
|
import { useCreateCategorizeFormSchema } from './use-form-schema';
|
||||||
|
import { useValues } from './use-values';
|
||||||
|
import { useWatchFormChange } from './use-watch-change';
|
||||||
|
|
||||||
|
const outputList = buildOutputList(initialCategorizeValues.outputs);
|
||||||
|
|
||||||
|
function CategorizeForm({ node }: INextOperatorForm) {
|
||||||
|
const values = useValues(node);
|
||||||
|
|
||||||
|
const FormSchema = useCreateCategorizeFormSchema();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: values,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<FormWrapper>
|
||||||
|
<FormContainer>
|
||||||
|
<QueryVariable></QueryVariable>
|
||||||
|
<LargeModelFormField></LargeModelFormField>
|
||||||
|
</FormContainer>
|
||||||
|
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
|
||||||
|
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>
|
||||||
|
<Output list={outputList}></Output>
|
||||||
|
</FormWrapper>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(CategorizeForm);
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export function useCreateCategorizeFormSchema() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
query: z.string().optional(),
|
||||||
|
parameter: z.string().optional(),
|
||||||
|
...LlmSettingSchema,
|
||||||
|
message_history_window_size: z.coerce.number(),
|
||||||
|
items: z.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1, t('flow.nameMessage')).trim(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
uuid: z.string(),
|
||||||
|
examples: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return FormSchema;
|
||||||
|
}
|
||||||
34
web/src/pages/data-flow/form/categorize-form/use-values.ts
Normal file
34
web/src/pages/data-flow/form/categorize-form/use-values.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { ModelVariableType } from '@/constants/knowledge';
|
||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { isEmpty, isPlainObject } from 'lodash';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
parameter: ModelVariableType.Precise,
|
||||||
|
message_history_window_size: 1,
|
||||||
|
temperatureEnabled: true,
|
||||||
|
topPEnabled: true,
|
||||||
|
presencePenaltyEnabled: true,
|
||||||
|
frequencyPenaltyEnabled: true,
|
||||||
|
maxTokensEnabled: true,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useValues(node?: RAGFlowNodeType) {
|
||||||
|
const values = useMemo(() => {
|
||||||
|
const formData = node?.data?.form;
|
||||||
|
if (isEmpty(formData)) {
|
||||||
|
return defaultValues;
|
||||||
|
}
|
||||||
|
if (isPlainObject(formData)) {
|
||||||
|
// const nextValues = {
|
||||||
|
// ...omit(formData, 'category_description'),
|
||||||
|
// items,
|
||||||
|
// };
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
}, [node]);
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
|
||||||
|
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
|
||||||
|
let values = useWatch({ control: form?.control });
|
||||||
|
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Manually triggered form updates are synchronized to the canvas
|
||||||
|
if (id) {
|
||||||
|
values = form?.getValues();
|
||||||
|
|
||||||
|
updateNodeForm(id, { ...values, items: values.items?.slice() || [] });
|
||||||
|
}
|
||||||
|
}, [id, updateNodeForm, values]);
|
||||||
|
}
|
||||||
168
web/src/pages/data-flow/form/code-form/index.tsx
Normal file
168
web/src/pages/data-flow/form/code-form/index.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import Editor, { loader } from '@monaco-editor/react';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
|
||||||
|
import { FormContainer } from '@/components/form-container';
|
||||||
|
import { useIsDarkTheme } from '@/components/theme-provider';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { RAGFlowSelect } from '@/components/ui/select';
|
||||||
|
import { ProgrammingLanguage } from '@/constants/agent';
|
||||||
|
import { ICodeForm } from '@/interfaces/database/agent';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
|
import { Output } from '../components/output';
|
||||||
|
import {
|
||||||
|
DynamicInputVariable,
|
||||||
|
TypeOptions,
|
||||||
|
VariableTitle,
|
||||||
|
} from './next-variable';
|
||||||
|
import { FormSchema, FormSchemaType } from './schema';
|
||||||
|
import { useValues } from './use-values';
|
||||||
|
import {
|
||||||
|
useHandleLanguageChange,
|
||||||
|
useWatchFormChange,
|
||||||
|
} from './use-watch-change';
|
||||||
|
|
||||||
|
loader.config({ paths: { vs: '/vs' } });
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
ProgrammingLanguage.Python,
|
||||||
|
ProgrammingLanguage.Javascript,
|
||||||
|
].map((x) => ({ value: x, label: x }));
|
||||||
|
|
||||||
|
const DynamicFieldName = 'outputs';
|
||||||
|
|
||||||
|
function CodeForm({ node }: INextOperatorForm) {
|
||||||
|
const formData = node?.data.form as ICodeForm;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const values = useValues(node);
|
||||||
|
const isDarkTheme = useIsDarkTheme();
|
||||||
|
|
||||||
|
const form = useForm<FormSchemaType>({
|
||||||
|
defaultValues: values,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
const handleLanguageChange = useHandleLanguageChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<FormWrapper>
|
||||||
|
<DynamicInputVariable
|
||||||
|
node={node}
|
||||||
|
title={t('flow.input')}
|
||||||
|
isOutputs={false}
|
||||||
|
></DynamicInputVariable>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="script"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center justify-between">
|
||||||
|
Code
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="lang"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<RAGFlowSelect
|
||||||
|
{...field}
|
||||||
|
onChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
handleLanguageChange(val);
|
||||||
|
}}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Editor
|
||||||
|
height={300}
|
||||||
|
theme={isDarkTheme ? 'vs-dark' : 'vs'}
|
||||||
|
language={formData.lang}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
automaticLayout: true,
|
||||||
|
}}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formData.lang === ProgrammingLanguage.Python ? (
|
||||||
|
<DynamicInputVariable
|
||||||
|
node={node}
|
||||||
|
title={'Return Values'}
|
||||||
|
name={DynamicFieldName}
|
||||||
|
isOutputs
|
||||||
|
></DynamicInputVariable>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<VariableTitle title={'Return Values'}></VariableTitle>
|
||||||
|
<FormContainer className="space-y-5">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`${DynamicFieldName}.name`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={t('common.pleaseInput')}
|
||||||
|
></Input>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`${DynamicFieldName}.type`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>Type</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RAGFlowSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={TypeOptions}
|
||||||
|
{...field}
|
||||||
|
></RAGFlowSelect>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormWrapper>
|
||||||
|
<div className="p-5">
|
||||||
|
<Output list={buildOutputList(formData.outputs)}></Output>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(CodeForm);
|
||||||
128
web/src/pages/data-flow/form/code-form/next-variable.tsx
Normal file
128
web/src/pages/data-flow/form/code-form/next-variable.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FormContainer } from '@/components/form-container';
|
||||||
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import { BlockButton, Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { BlurInput } from '@/components/ui/input';
|
||||||
|
import { RAGFlowSelect } from '@/components/ui/select';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node?: RAGFlowNodeType;
|
||||||
|
name?: string;
|
||||||
|
isOutputs: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TypeOptions = [
|
||||||
|
'String',
|
||||||
|
'Number',
|
||||||
|
'Boolean',
|
||||||
|
'Array<String>',
|
||||||
|
'Array<Number>',
|
||||||
|
'Object',
|
||||||
|
].map((x) => ({ label: x, value: x }));
|
||||||
|
|
||||||
|
export function DynamicVariableForm({ name = 'arguments', isOutputs }: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext();
|
||||||
|
|
||||||
|
const { fields, remove, append } = useFieldArray({
|
||||||
|
name: name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextOptions = useBuildQueryVariableOptions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-5">
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const typeField = `${name}.${index}.name`;
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="flex w-full items-center gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={typeField}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1 overflow-hidden">
|
||||||
|
<FormControl>
|
||||||
|
<BlurInput
|
||||||
|
{...field}
|
||||||
|
placeholder={t('common.pleaseInput')}
|
||||||
|
></BlurInput>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Separator className="w-3 text-text-secondary" />
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`${name}.${index}.type`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1 overflow-hidden">
|
||||||
|
<FormControl>
|
||||||
|
{isOutputs ? (
|
||||||
|
<RAGFlowSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={TypeOptions}
|
||||||
|
{...field}
|
||||||
|
></RAGFlowSelect>
|
||||||
|
) : (
|
||||||
|
<SelectWithSearch
|
||||||
|
options={nextOptions}
|
||||||
|
{...field}
|
||||||
|
></SelectWithSearch>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||||
|
<X className="text-text-sub-title-invert " />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<BlockButton onClick={() => append({ name: '', type: undefined })}>
|
||||||
|
{t('flow.addVariable')}
|
||||||
|
</BlockButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariableTitle({ title }: { title: ReactNode }) {
|
||||||
|
return <div className="font-medium text-text-primary pb-2">{title}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DynamicInputVariable({
|
||||||
|
node,
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
isOutputs = false,
|
||||||
|
}: IProps & { title: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<VariableTitle title={title}></VariableTitle>
|
||||||
|
<FormContainer>
|
||||||
|
<DynamicVariableForm
|
||||||
|
node={node}
|
||||||
|
name={name}
|
||||||
|
isOutputs={isOutputs}
|
||||||
|
></DynamicVariableForm>
|
||||||
|
</FormContainer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
web/src/pages/data-flow/form/code-form/schema.ts
Normal file
14
web/src/pages/data-flow/form/code-form/schema.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { ProgrammingLanguage } from '@/constants/agent';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const FormSchema = z.object({
|
||||||
|
lang: z.enum([ProgrammingLanguage.Python, ProgrammingLanguage.Javascript]),
|
||||||
|
script: z.string(),
|
||||||
|
arguments: z.array(z.object({ name: z.string(), type: z.string() })),
|
||||||
|
outputs: z.union([
|
||||||
|
z.array(z.object({ name: z.string(), type: z.string() })).optional(),
|
||||||
|
z.object({ name: z.string(), type: z.string() }),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormSchemaType = z.infer<typeof FormSchema>;
|
||||||
47
web/src/pages/data-flow/form/code-form/use-values.ts
Normal file
47
web/src/pages/data-flow/form/code-form/use-values.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { ProgrammingLanguage } from '@/constants/agent';
|
||||||
|
import { ICodeForm } from '@/interfaces/database/agent';
|
||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { initialCodeValues } from '../../constant';
|
||||||
|
|
||||||
|
function convertToArray(args: Record<string, string>) {
|
||||||
|
return Object.entries(args).map(([key, value]) => ({
|
||||||
|
name: key,
|
||||||
|
type: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputsFormType = { name: string; type: string };
|
||||||
|
|
||||||
|
function convertOutputsToArray({ lang, outputs = {} }: ICodeForm) {
|
||||||
|
if (lang === ProgrammingLanguage.Python) {
|
||||||
|
return Object.entries(outputs).map(([key, val]) => ({
|
||||||
|
name: key,
|
||||||
|
type: val.type,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return Object.entries(outputs).reduce<OutputsFormType>((pre, [key, val]) => {
|
||||||
|
pre.name = key;
|
||||||
|
pre.type = val.type;
|
||||||
|
return pre;
|
||||||
|
}, {} as OutputsFormType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useValues(node?: RAGFlowNodeType) {
|
||||||
|
const values = useMemo(() => {
|
||||||
|
const formData = node?.data?.form;
|
||||||
|
|
||||||
|
if (isEmpty(formData)) {
|
||||||
|
return initialCodeValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...formData,
|
||||||
|
arguments: convertToArray(formData.arguments),
|
||||||
|
outputs: convertOutputsToArray(formData),
|
||||||
|
};
|
||||||
|
}, [node?.data?.form]);
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
95
web/src/pages/data-flow/form/code-form/use-watch-change.ts
Normal file
95
web/src/pages/data-flow/form/code-form/use-watch-change.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent';
|
||||||
|
import { ICodeForm } from '@/interfaces/database/agent';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||||
|
import useGraphStore from '../../store';
|
||||||
|
import { FormSchemaType } from './schema';
|
||||||
|
|
||||||
|
function convertToObject(list: FormSchemaType['arguments'] = []) {
|
||||||
|
return list.reduce<Record<string, string>>((pre, cur) => {
|
||||||
|
pre[cur.name] = cur.type;
|
||||||
|
|
||||||
|
return pre;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArrayOutputs = Extract<FormSchemaType['outputs'], Array<any>>;
|
||||||
|
|
||||||
|
type ObjectOutputs = Exclude<FormSchemaType['outputs'], Array<any>>;
|
||||||
|
|
||||||
|
function convertOutputsToObject({ lang, outputs }: FormSchemaType) {
|
||||||
|
if (lang === ProgrammingLanguage.Python) {
|
||||||
|
return (outputs as ArrayOutputs).reduce<ICodeForm['outputs']>(
|
||||||
|
(pre, cur) => {
|
||||||
|
pre[cur.name] = {
|
||||||
|
value: '',
|
||||||
|
type: cur.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
return pre;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const outputsObject = outputs as ObjectOutputs;
|
||||||
|
if (isEmpty(outputsObject)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
[outputsObject.name]: {
|
||||||
|
value: '',
|
||||||
|
type: outputsObject.type,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWatchFormChange(
|
||||||
|
id?: string,
|
||||||
|
form?: UseFormReturn<FormSchemaType>,
|
||||||
|
) {
|
||||||
|
let values = useWatch({ control: form?.control });
|
||||||
|
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Manually triggered form updates are synchronized to the canvas
|
||||||
|
if (id) {
|
||||||
|
values = form?.getValues() || {};
|
||||||
|
let nextValues: any = {
|
||||||
|
...values,
|
||||||
|
arguments: convertToObject(
|
||||||
|
values?.arguments as FormSchemaType['arguments'],
|
||||||
|
),
|
||||||
|
outputs: convertOutputsToObject(values as FormSchemaType),
|
||||||
|
};
|
||||||
|
|
||||||
|
updateNodeForm(id, nextValues);
|
||||||
|
}
|
||||||
|
}, [form?.formState.isDirty, id, updateNodeForm, values]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHandleLanguageChange(
|
||||||
|
id?: string,
|
||||||
|
form?: UseFormReturn<FormSchemaType>,
|
||||||
|
) {
|
||||||
|
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||||
|
|
||||||
|
const handleLanguageChange = useCallback(
|
||||||
|
(lang: string) => {
|
||||||
|
if (id) {
|
||||||
|
const script = CodeTemplateStrMap[lang as ProgrammingLanguage];
|
||||||
|
form?.setValue('script', script);
|
||||||
|
form?.setValue(
|
||||||
|
'outputs',
|
||||||
|
(lang === ProgrammingLanguage.Python
|
||||||
|
? []
|
||||||
|
: {}) as FormSchemaType['outputs'],
|
||||||
|
);
|
||||||
|
updateNodeForm(id, script, ['script']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[form, id, updateNodeForm],
|
||||||
|
);
|
||||||
|
|
||||||
|
return handleLanguageChange;
|
||||||
|
}
|
||||||
32
web/src/pages/data-flow/form/components/api-key-field.tsx
Normal file
32
web/src/pages/data-flow/form/components/api-key-field.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
interface IApiKeyFieldProps {
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
export function ApiKeyField({ placeholder }: IApiKeyFieldProps) {
|
||||||
|
const form = useFormContext();
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('flow.apiKey')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" {...field} placeholder={placeholder}></Input>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
export function DescriptionField() {
|
||||||
|
const form = useFormContext();
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`description`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormLabel>{t('flow.description')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea {...field}></Textarea>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Collapse, Flex, Form, Input, Select } from 'antd';
|
||||||
|
import { PropsWithChildren, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useBuildVariableOptions } from '../../hooks/use-get-begin-query';
|
||||||
|
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node?: RAGFlowNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VariableType {
|
||||||
|
Reference = 'reference',
|
||||||
|
Input = 'input',
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVariableName = (type: string) =>
|
||||||
|
type === VariableType.Reference ? 'component_id' : 'value';
|
||||||
|
|
||||||
|
const DynamicVariableForm = ({ node }: IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const valueOptions = useBuildVariableOptions(node?.id, node?.parentId);
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ value: VariableType.Reference, label: t('flow.reference') },
|
||||||
|
{ value: VariableType.Input, label: t('flow.text') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTypeChange = useCallback(
|
||||||
|
(name: number) => () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
form.setFieldValue(['query', name, 'component_id'], undefined);
|
||||||
|
form.setFieldValue(['query', name, 'value'], undefined);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
[form],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.List name="query">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
|
<Flex key={key} gap={10} align={'baseline'}>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'type']}
|
||||||
|
className={styles.variableType}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={options}
|
||||||
|
onChange={handleTypeChange(name)}
|
||||||
|
></Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item noStyle dependencies={[name, 'type']}>
|
||||||
|
{({ getFieldValue }) => {
|
||||||
|
const type = getFieldValue(['query', name, 'type']);
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, getVariableName(type)]}
|
||||||
|
className={styles.variableValue}
|
||||||
|
>
|
||||||
|
{type === VariableType.Reference ? (
|
||||||
|
<Select
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={valueOptions}
|
||||||
|
></Select>
|
||||||
|
) : (
|
||||||
|
<Input placeholder={t('common.pleaseInput')} />
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => add({ type: VariableType.Reference })}
|
||||||
|
block
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
className={styles.addButton}
|
||||||
|
>
|
||||||
|
{t('flow.addVariable')}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FormCollapse({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
}: PropsWithChildren<{ title: string }>) {
|
||||||
|
return (
|
||||||
|
<Collapse
|
||||||
|
className={styles.dynamicInputVariable}
|
||||||
|
defaultActiveKey={['1']}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
label: <span className={styles.title}>{title}</span>,
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DynamicInputVariable = ({ node }: IProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<FormCollapse title={t('flow.input')}>
|
||||||
|
<DynamicVariableForm node={node}></DynamicVariableForm>
|
||||||
|
</FormCollapse>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicInputVariable;
|
||||||
16
web/src/pages/data-flow/form/components/form-wrapper.tsx
Normal file
16
web/src/pages/data-flow/form/components/form-wrapper.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
type FormProps = React.ComponentProps<'form'>;
|
||||||
|
|
||||||
|
export function FormWrapper({ children, ...props }: FormProps) {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="space-y-6 p-4"
|
||||||
|
autoComplete="off"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
web/src/pages/data-flow/form/components/index.less
Normal file
22
web/src/pages/data-flow/form/components/index.less
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.dynamicInputVariable {
|
||||||
|
background-color: #ebe9e950;
|
||||||
|
:global(.ant-collapse-content) {
|
||||||
|
background-color: #f6f6f657;
|
||||||
|
}
|
||||||
|
margin-bottom: 20px;
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.variableType {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
.variableValue {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
color: rgb(22, 119, 255);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { SideDown } from '@/assets/icon/next-icon';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { RAGFlowSelect } from '@/components/ui/select';
|
||||||
|
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||||
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useBuildVariableOptions } from '../../hooks/use-get-begin-query';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node?: RAGFlowNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VariableType {
|
||||||
|
Reference = 'reference',
|
||||||
|
Input = 'input',
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVariableName = (type: string) =>
|
||||||
|
type === VariableType.Reference ? 'component_id' : 'value';
|
||||||
|
|
||||||
|
export function DynamicVariableForm({ node }: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext();
|
||||||
|
const { fields, remove, append } = useFieldArray({
|
||||||
|
name: 'query',
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
const valueOptions = useBuildVariableOptions(node?.id, node?.parentId);
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ value: VariableType.Reference, label: t('flow.reference') },
|
||||||
|
{ value: VariableType.Input, label: t('flow.text') },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const typeField = `query.${index}.type`;
|
||||||
|
const typeValue = form.watch(typeField);
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="flex items-center gap-1">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={typeField}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-2/5">
|
||||||
|
<FormDescription />
|
||||||
|
<FormControl>
|
||||||
|
<RAGFlowSelect
|
||||||
|
{...field}
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={options}
|
||||||
|
onChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
form.resetField(`query.${index}.value`);
|
||||||
|
form.resetField(`query.${index}.component_id`);
|
||||||
|
}}
|
||||||
|
></RAGFlowSelect>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`query.${index}.${getVariableName(typeValue)}`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormDescription />
|
||||||
|
<FormControl>
|
||||||
|
{typeValue === VariableType.Reference ? (
|
||||||
|
<RAGFlowSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
{...field}
|
||||||
|
options={valueOptions}
|
||||||
|
></RAGFlowSelect>
|
||||||
|
) : (
|
||||||
|
<Input placeholder={t('common.pleaseInput')} {...field} />
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Trash2
|
||||||
|
className="cursor-pointer mx-3 size-4 text-colors-text-functional-danger"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button onClick={append} className="mt-4" variant={'outline'} size={'sm'}>
|
||||||
|
<Plus />
|
||||||
|
{t('flow.addVariable')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DynamicInputVariable({ node }: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible defaultOpen className="group/collapsible">
|
||||||
|
<CollapsibleTrigger className="flex justify-between w-full pb-2">
|
||||||
|
<span className="font-bold text-2xl text-colors-text-neutral-strong">
|
||||||
|
{t('flow.input')}
|
||||||
|
</span>
|
||||||
|
<Button variant={'icon'} size={'icon'}>
|
||||||
|
<SideDown />
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<DynamicVariableForm node={node}></DynamicVariableForm>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
web/src/pages/data-flow/form/components/output.tsx
Normal file
35
web/src/pages/data-flow/form/components/output.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { t } from 'i18next';
|
||||||
|
|
||||||
|
export type OutputType = {
|
||||||
|
title: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OutputProps = {
|
||||||
|
list: Array<OutputType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function transferOutputs(outputs: Record<string, any>) {
|
||||||
|
return Object.entries(outputs).map(([key, value]) => ({
|
||||||
|
title: key,
|
||||||
|
type: value?.type,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Output({ list }: OutputProps) {
|
||||||
|
return (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<div>{t('flow.output')}</div>
|
||||||
|
<ul>
|
||||||
|
{list.map((x, idx) => (
|
||||||
|
<li
|
||||||
|
key={idx}
|
||||||
|
className="bg-background-highlight text-accent-primary rounded-sm px-2 py-1"
|
||||||
|
>
|
||||||
|
{x.title}: <span className="text-text-secondary">{x.type}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const ProgrammaticTag = 'programmatic';
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
.typeahead-popover {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeahead-popover ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeahead-popover ul::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeahead-popover ul {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeahead-popover ul li {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 180px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeahead-popover ul li.selected {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeahead-popover li {
|
||||||
|
margin: 0 8px 0 8px;
|
||||||
|
color: #050505;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeahead-popover li.active {
|
||||||
|
display: flex;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeahead-popover li .text {
|
||||||
|
display: flex;
|
||||||
|
line-height: 20px;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeahead-popover li .icon {
|
||||||
|
display: flex;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
user-select: none;
|
||||||
|
margin-right: 8px;
|
||||||
|
line-height: 16px;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
181
web/src/pages/data-flow/form/components/prompt-editor/index.tsx
Normal file
181
web/src/pages/data-flow/form/components/prompt-editor/index.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import { CodeHighlightNode, CodeNode } from '@lexical/code';
|
||||||
|
import {
|
||||||
|
InitialConfigType,
|
||||||
|
LexicalComposer,
|
||||||
|
} from '@lexical/react/LexicalComposer';
|
||||||
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||||
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
|
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
||||||
|
import {
|
||||||
|
$getRoot,
|
||||||
|
$getSelection,
|
||||||
|
EditorState,
|
||||||
|
Klass,
|
||||||
|
LexicalNode,
|
||||||
|
} from 'lexical';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { Variable } from 'lucide-react';
|
||||||
|
import { ReactNode, useCallback, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PasteHandlerPlugin } from './paste-handler-plugin';
|
||||||
|
import theme from './theme';
|
||||||
|
import { VariableNode } from './variable-node';
|
||||||
|
import { VariableOnChangePlugin } from './variable-on-change-plugin';
|
||||||
|
import VariablePickerMenuPlugin from './variable-picker-plugin';
|
||||||
|
|
||||||
|
// Catch any errors that occur during Lexical updates and log them
|
||||||
|
// or throw them as needed. If you don't throw them, Lexical will
|
||||||
|
// try to recover gracefully without losing user data.
|
||||||
|
function onError(error: Error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Nodes: Array<Klass<LexicalNode>> = [
|
||||||
|
HeadingNode,
|
||||||
|
QuoteNode,
|
||||||
|
CodeHighlightNode,
|
||||||
|
CodeNode,
|
||||||
|
VariableNode,
|
||||||
|
];
|
||||||
|
|
||||||
|
type PromptContentProps = { showToolbar?: boolean; multiLine?: boolean };
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value?: string) => void;
|
||||||
|
placeholder?: ReactNode;
|
||||||
|
} & PromptContentProps;
|
||||||
|
|
||||||
|
function PromptContent({
|
||||||
|
showToolbar = true,
|
||||||
|
multiLine = true,
|
||||||
|
}: PromptContentProps) {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const [isBlur, setIsBlur] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const insertTextAtCursor = useCallback(() => {
|
||||||
|
editor.update(() => {
|
||||||
|
const selection = $getSelection();
|
||||||
|
|
||||||
|
if (selection !== null) {
|
||||||
|
selection.insertText(' /');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const handleVariableIconClick = useCallback(() => {
|
||||||
|
insertTextAtCursor();
|
||||||
|
}, [insertTextAtCursor]);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
setIsBlur(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
setIsBlur(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cn('border rounded-sm ', { 'border-blue-400': !isBlur })}
|
||||||
|
>
|
||||||
|
{showToolbar && (
|
||||||
|
<div className="border-b px-2 py-2 justify-end flex">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="inline-block cursor-pointer cursor p-0.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-sm">
|
||||||
|
<Variable size={16} onClick={handleVariableIconClick} />
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t('flow.insertVariableTip')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ContentEditable
|
||||||
|
className={cn(
|
||||||
|
'relative px-2 py-1 focus-visible:outline-none max-h-[50vh] overflow-auto',
|
||||||
|
{
|
||||||
|
'min-h-40': multiLine,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PromptEditor({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
showToolbar,
|
||||||
|
multiLine = true,
|
||||||
|
}: IProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const initialConfig: InitialConfigType = {
|
||||||
|
namespace: 'PromptEditor',
|
||||||
|
theme,
|
||||||
|
onError,
|
||||||
|
nodes: Nodes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const onValueChange = useCallback(
|
||||||
|
(editorState: EditorState) => {
|
||||||
|
editorState?.read(() => {
|
||||||
|
// const listNodes = $nodesOfType(VariableNode); // to be removed
|
||||||
|
// const allNodes = $dfs();
|
||||||
|
|
||||||
|
const text = $getRoot().getTextContent();
|
||||||
|
|
||||||
|
onChange?.(text);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<LexicalComposer initialConfig={initialConfig}>
|
||||||
|
<RichTextPlugin
|
||||||
|
contentEditable={
|
||||||
|
<PromptContent
|
||||||
|
showToolbar={showToolbar}
|
||||||
|
multiLine={multiLine}
|
||||||
|
></PromptContent>
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute top-1 left-2 text-text-secondary pointer-events-none',
|
||||||
|
{
|
||||||
|
'truncate w-[90%]': !multiLine,
|
||||||
|
'translate-y-10': multiLine,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{placeholder || t('common.promptPlaceholder')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
|
/>
|
||||||
|
<VariablePickerMenuPlugin value={value}></VariablePickerMenuPlugin>
|
||||||
|
<PasteHandlerPlugin />
|
||||||
|
<VariableOnChangePlugin
|
||||||
|
onChange={onValueChange}
|
||||||
|
></VariableOnChangePlugin>
|
||||||
|
</LexicalComposer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import {
|
||||||
|
$createParagraphNode,
|
||||||
|
$createTextNode,
|
||||||
|
$getSelection,
|
||||||
|
$isRangeSelection,
|
||||||
|
PASTE_COMMAND,
|
||||||
|
} from 'lexical';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
function PasteHandlerPlugin() {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
useEffect(() => {
|
||||||
|
const removeListener = editor.registerCommand(
|
||||||
|
PASTE_COMMAND,
|
||||||
|
(clipboardEvent: ClipboardEvent) => {
|
||||||
|
const clipboardData = clipboardEvent.clipboardData;
|
||||||
|
if (!clipboardData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = clipboardData.getData('text/plain');
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if text contains line breaks
|
||||||
|
if (text.includes('\n')) {
|
||||||
|
editor.update(() => {
|
||||||
|
const selection = $getSelection();
|
||||||
|
if (selection && $isRangeSelection(selection)) {
|
||||||
|
// Normalize line breaks, merge multiple consecutive line breaks into a single line break
|
||||||
|
const normalizedText = text.replace(/\n{2,}/g, '\n');
|
||||||
|
|
||||||
|
// Clear current selection
|
||||||
|
selection.removeText();
|
||||||
|
|
||||||
|
// Create a paragraph node to contain all content
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
|
||||||
|
// Split text by line breaks
|
||||||
|
const lines = normalizedText.split('\n');
|
||||||
|
|
||||||
|
// Process each line
|
||||||
|
lines.forEach((lineText, index) => {
|
||||||
|
// Add line text (if any)
|
||||||
|
if (lineText) {
|
||||||
|
const textNode = $createTextNode(lineText);
|
||||||
|
paragraph.append(textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not the last line, add a line break
|
||||||
|
if (index < lines.length - 1) {
|
||||||
|
const lineBreak = $createTextNode('\n');
|
||||||
|
paragraph.append(lineBreak);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert paragraph
|
||||||
|
selection.insertNodes([paragraph]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent default paste behavior
|
||||||
|
clipboardEvent.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no line breaks, use default behavior
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeListener();
|
||||||
|
};
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PasteHandlerPlugin };
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
code: 'editor-code',
|
||||||
|
heading: {
|
||||||
|
h1: 'editor-heading-h1',
|
||||||
|
h2: 'editor-heading-h2',
|
||||||
|
h3: 'editor-heading-h3',
|
||||||
|
h4: 'editor-heading-h4',
|
||||||
|
h5: 'editor-heading-h5',
|
||||||
|
},
|
||||||
|
image: 'editor-image',
|
||||||
|
link: 'editor-link',
|
||||||
|
list: {
|
||||||
|
listitem: 'editor-listitem',
|
||||||
|
nested: {
|
||||||
|
listitem: 'editor-nested-listitem',
|
||||||
|
},
|
||||||
|
ol: 'editor-list-ol',
|
||||||
|
ul: 'editor-list-ul',
|
||||||
|
},
|
||||||
|
ltr: 'ltr',
|
||||||
|
paragraph: 'editor-paragraph',
|
||||||
|
placeholder: 'editor-placeholder',
|
||||||
|
quote: 'editor-quote',
|
||||||
|
rtl: 'rtl',
|
||||||
|
text: {
|
||||||
|
bold: 'editor-text-bold',
|
||||||
|
code: 'editor-text-code',
|
||||||
|
hashtag: 'editor-text-hashtag',
|
||||||
|
italic: 'editor-text-italic',
|
||||||
|
overflowed: 'editor-text-overflowed',
|
||||||
|
strikethrough: 'editor-text-strikethrough',
|
||||||
|
underline: 'editor-text-underline',
|
||||||
|
underlineStrikethrough: 'editor-text-underlineStrikethrough',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
import { BeginId } from '@/pages/flow/constant';
|
||||||
|
import { DecoratorNode, LexicalNode, NodeKey } from 'lexical';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
const prefix = BeginId + '@';
|
||||||
|
|
||||||
|
export class VariableNode extends DecoratorNode<ReactNode> {
|
||||||
|
__value: string;
|
||||||
|
__label: string;
|
||||||
|
key?: NodeKey;
|
||||||
|
__parentLabel?: string | ReactNode;
|
||||||
|
__icon?: ReactNode;
|
||||||
|
|
||||||
|
static getType(): string {
|
||||||
|
return 'variable';
|
||||||
|
}
|
||||||
|
|
||||||
|
static clone(node: VariableNode): VariableNode {
|
||||||
|
return new VariableNode(
|
||||||
|
node.__value,
|
||||||
|
node.__label,
|
||||||
|
node.__key,
|
||||||
|
node.__parentLabel,
|
||||||
|
node.__icon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
value: string,
|
||||||
|
label: string,
|
||||||
|
key?: NodeKey,
|
||||||
|
parent?: string | ReactNode,
|
||||||
|
icon?: ReactNode,
|
||||||
|
) {
|
||||||
|
super(key);
|
||||||
|
this.__value = value;
|
||||||
|
this.__label = label;
|
||||||
|
this.__parentLabel = parent;
|
||||||
|
this.__icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
createDOM(): HTMLElement {
|
||||||
|
const dom = document.createElement('span');
|
||||||
|
dom.className = 'mr-1';
|
||||||
|
|
||||||
|
return dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDOM(): false {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
decorate(): ReactNode {
|
||||||
|
let content: ReactNode = (
|
||||||
|
<div className="text-blue-600">{this.__label}</div>
|
||||||
|
);
|
||||||
|
if (this.__parentLabel) {
|
||||||
|
content = (
|
||||||
|
<div className="flex items-center gap-1 text-text-primary ">
|
||||||
|
<div>{this.__icon}</div>
|
||||||
|
<div>{this.__parentLabel}</div>
|
||||||
|
<div className="text-text-disabled mr-1">/</div>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-200 dark:bg-gray-400 text-sm inline-flex items-center rounded-md px-2 py-1">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextContent(): string {
|
||||||
|
return `{${this.__value}}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $createVariableNode(
|
||||||
|
value: string,
|
||||||
|
label: string,
|
||||||
|
parentLabel: string | ReactNode,
|
||||||
|
icon?: ReactNode,
|
||||||
|
): VariableNode {
|
||||||
|
return new VariableNode(value, label, undefined, parentLabel, icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $isVariableNode(
|
||||||
|
node: LexicalNode | null | undefined,
|
||||||
|
): node is VariableNode {
|
||||||
|
return node instanceof VariableNode;
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { EditorState, LexicalEditor } from 'lexical';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { ProgrammaticTag } from './constant';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onChange: (
|
||||||
|
editorState: EditorState,
|
||||||
|
editor?: LexicalEditor,
|
||||||
|
tags?: Set<string>,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariableOnChangePlugin({ onChange }: IProps) {
|
||||||
|
// Access the editor through the LexicalComposerContext
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
// Wrap our listener in useEffect to handle the teardown and avoid stale references.
|
||||||
|
useEffect(() => {
|
||||||
|
// most listeners return a teardown function that can be called to clean them up.
|
||||||
|
return editor.registerUpdateListener(
|
||||||
|
({ editorState, tags, dirtyElements }) => {
|
||||||
|
// Check if there is a "programmatic" tag
|
||||||
|
const isProgrammaticUpdate = tags.has(ProgrammaticTag);
|
||||||
|
|
||||||
|
// The onchange event is only triggered when the data is manually updated
|
||||||
|
// Otherwise, the content will be displayed incorrectly.
|
||||||
|
if (dirtyElements.size > 0 && !isProgrammaticUpdate) {
|
||||||
|
onChange(editorState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [editor, onChange]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -0,0 +1,297 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import {
|
||||||
|
LexicalTypeaheadMenuPlugin,
|
||||||
|
MenuOption,
|
||||||
|
useBasicTypeaheadTriggerMatch,
|
||||||
|
} from '@lexical/react/LexicalTypeaheadMenuPlugin';
|
||||||
|
import {
|
||||||
|
$createParagraphNode,
|
||||||
|
$createTextNode,
|
||||||
|
$getRoot,
|
||||||
|
$getSelection,
|
||||||
|
$isRangeSelection,
|
||||||
|
TextNode,
|
||||||
|
} from 'lexical';
|
||||||
|
import React, {
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import * as ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import { $createVariableNode } from './variable-node';
|
||||||
|
|
||||||
|
import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query';
|
||||||
|
import { ProgrammaticTag } from './constant';
|
||||||
|
import './index.css';
|
||||||
|
class VariableInnerOption extends MenuOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
parentLabel: string | JSX.Element;
|
||||||
|
icon?: ReactNode;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
label: string,
|
||||||
|
value: string,
|
||||||
|
parentLabel: string | JSX.Element,
|
||||||
|
icon?: ReactNode,
|
||||||
|
) {
|
||||||
|
super(value);
|
||||||
|
this.label = label;
|
||||||
|
this.value = value;
|
||||||
|
this.parentLabel = parentLabel;
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VariableOption extends MenuOption {
|
||||||
|
label: ReactElement | string;
|
||||||
|
title: string;
|
||||||
|
options: VariableInnerOption[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
label: ReactElement | string,
|
||||||
|
title: string,
|
||||||
|
options: VariableInnerOption[],
|
||||||
|
) {
|
||||||
|
super(title);
|
||||||
|
this.label = label;
|
||||||
|
this.title = title;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function VariablePickerMenuItem({
|
||||||
|
index,
|
||||||
|
option,
|
||||||
|
selectOptionAndCleanUp,
|
||||||
|
}: {
|
||||||
|
index: number;
|
||||||
|
option: VariableOption;
|
||||||
|
selectOptionAndCleanUp: (
|
||||||
|
option: VariableOption | VariableInnerOption,
|
||||||
|
) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={option.key}
|
||||||
|
tabIndex={-1}
|
||||||
|
ref={option.setRefElement}
|
||||||
|
role="option"
|
||||||
|
id={'typeahead-item-' + index}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="text text-slate-500">{option.title}</span>
|
||||||
|
<ul className="pl-2 py-1">
|
||||||
|
{option.options.map((x) => (
|
||||||
|
<li
|
||||||
|
key={x.value}
|
||||||
|
onClick={() => selectOptionAndCleanUp(x)}
|
||||||
|
className="hover:bg-slate-300 p-1"
|
||||||
|
>
|
||||||
|
{x.label}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VariablePickerMenuPlugin({
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
value?: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const isFirstRender = useRef(true);
|
||||||
|
|
||||||
|
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
||||||
|
minLength: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [queryString, setQueryString] = React.useState<string | null>('');
|
||||||
|
|
||||||
|
const options = useBuildQueryVariableOptions();
|
||||||
|
|
||||||
|
const buildNextOptions = useCallback(() => {
|
||||||
|
let filteredOptions = options;
|
||||||
|
if (queryString) {
|
||||||
|
const lowerQuery = queryString.toLowerCase();
|
||||||
|
filteredOptions = options
|
||||||
|
.map((x) => ({
|
||||||
|
...x,
|
||||||
|
options: x.options.filter(
|
||||||
|
(y) =>
|
||||||
|
y.label.toLowerCase().includes(lowerQuery) ||
|
||||||
|
y.value.toLowerCase().includes(lowerQuery),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter((x) => x.options.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextOptions: VariableOption[] = filteredOptions.map(
|
||||||
|
(x) =>
|
||||||
|
new VariableOption(
|
||||||
|
x.label,
|
||||||
|
x.title,
|
||||||
|
x.options.map((y) => {
|
||||||
|
return new VariableInnerOption(y.label, y.value, x.label, y.icon);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return nextOptions;
|
||||||
|
}, [options, queryString]);
|
||||||
|
|
||||||
|
const findItemByValue = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const children = options.reduce<
|
||||||
|
Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
parentLabel?: string | ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
}>
|
||||||
|
>((pre, cur) => {
|
||||||
|
return pre.concat(cur.options);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return children.find((x) => x.value === value);
|
||||||
|
},
|
||||||
|
[options],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectOption = useCallback(
|
||||||
|
(
|
||||||
|
selectedOption: VariableOption | VariableInnerOption,
|
||||||
|
nodeToRemove: TextNode | null,
|
||||||
|
closeMenu: () => void,
|
||||||
|
) => {
|
||||||
|
editor.update(() => {
|
||||||
|
const selection = $getSelection();
|
||||||
|
|
||||||
|
if (!$isRangeSelection(selection) || selectedOption === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeToRemove) {
|
||||||
|
nodeToRemove.remove();
|
||||||
|
}
|
||||||
|
const variableNode = $createVariableNode(
|
||||||
|
(selectedOption as VariableInnerOption).value,
|
||||||
|
selectedOption.label as string,
|
||||||
|
selectedOption.parentLabel as string | ReactNode,
|
||||||
|
selectedOption.icon as ReactNode,
|
||||||
|
);
|
||||||
|
selection.insertNodes([variableNode]);
|
||||||
|
|
||||||
|
closeMenu();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[editor],
|
||||||
|
);
|
||||||
|
|
||||||
|
const parseTextToVariableNodes = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
|
||||||
|
// Regular expression to match content within {}
|
||||||
|
const regex = /{([^}]*)}/g;
|
||||||
|
let match;
|
||||||
|
let lastIndex = 0;
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
const { 1: content, index, 0: template } = match;
|
||||||
|
|
||||||
|
// Add the previous text part (if any)
|
||||||
|
if (index > lastIndex) {
|
||||||
|
const textNode = $createTextNode(text.slice(lastIndex, index));
|
||||||
|
|
||||||
|
paragraph.append(textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add variable node or text node
|
||||||
|
const nodeItem = findItemByValue(content);
|
||||||
|
|
||||||
|
if (nodeItem) {
|
||||||
|
paragraph.append(
|
||||||
|
$createVariableNode(
|
||||||
|
content,
|
||||||
|
nodeItem.label,
|
||||||
|
nodeItem.parentLabel,
|
||||||
|
nodeItem.icon,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
paragraph.append($createTextNode(template));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update index
|
||||||
|
lastIndex = regex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last part of text (if any)
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
const textNode = $createTextNode(text.slice(lastIndex));
|
||||||
|
paragraph.append(textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$getRoot().clear().append(paragraph);
|
||||||
|
|
||||||
|
if ($isRangeSelection($getSelection())) {
|
||||||
|
$getRoot().selectEnd();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[findItemByValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor && value && isFirstRender.current) {
|
||||||
|
isFirstRender.current = false;
|
||||||
|
editor.update(
|
||||||
|
() => {
|
||||||
|
parseTextToVariableNodes(value);
|
||||||
|
},
|
||||||
|
{ tag: ProgrammaticTag },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [parseTextToVariableNodes, editor, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption>
|
||||||
|
onQueryChange={setQueryString}
|
||||||
|
onSelectOption={onSelectOption}
|
||||||
|
triggerFn={checkForTriggerMatch}
|
||||||
|
options={buildNextOptions()}
|
||||||
|
menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => {
|
||||||
|
const nextOptions = buildNextOptions();
|
||||||
|
return anchorElementRef.current && nextOptions.length
|
||||||
|
? ReactDOM.createPortal(
|
||||||
|
<div className="typeahead-popover w-[200px] p-2">
|
||||||
|
<ul className="overflow-y-auto !scrollbar-thin overflow-x-hidden">
|
||||||
|
{nextOptions.map((option, i: number) => (
|
||||||
|
<VariablePickerMenuItem
|
||||||
|
index={i}
|
||||||
|
key={option.key}
|
||||||
|
option={option}
|
||||||
|
selectOptionAndCleanUp={selectOptionAndCleanUp}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>,
|
||||||
|
anchorElementRef.current,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
web/src/pages/data-flow/form/components/query-variable.tsx
Normal file
66
web/src/pages/data-flow/form/components/query-variable.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { toLower } from 'lodash';
|
||||||
|
import { ReactNode, useMemo } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { VariableType } from '../../constant';
|
||||||
|
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
|
||||||
|
|
||||||
|
type QueryVariableProps = {
|
||||||
|
name?: string;
|
||||||
|
type?: VariableType;
|
||||||
|
label?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function QueryVariable({
|
||||||
|
name = 'query',
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
}: QueryVariableProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext();
|
||||||
|
|
||||||
|
const nextOptions = useBuildQueryVariableOptions();
|
||||||
|
|
||||||
|
const finalOptions = useMemo(() => {
|
||||||
|
return type
|
||||||
|
? nextOptions.map((x) => {
|
||||||
|
return {
|
||||||
|
...x,
|
||||||
|
options: x.options.filter((y) => toLower(y.type).includes(type)),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: nextOptions;
|
||||||
|
}, [nextOptions, type]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
{label || (
|
||||||
|
<FormLabel tooltip={t('flow.queryTip')}>
|
||||||
|
{t('flow.query')}
|
||||||
|
</FormLabel>
|
||||||
|
)}
|
||||||
|
<FormControl>
|
||||||
|
<SelectWithSearch
|
||||||
|
options={finalOptions}
|
||||||
|
{...field}
|
||||||
|
allowClear
|
||||||
|
></SelectWithSearch>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user