Feat: Delete flow related code. #9869 (#10371)

### What problem does this PR solve?

Feat: Delete flow related code. #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-09-30 12:00:17 +08:00
committed by GitHub
parent 9989e06abb
commit fb19e24f8a
148 changed files with 1 additions and 15150 deletions

View File

@ -1,79 +1,14 @@
import { ResponseType } from '@/interfaces/database/base';
import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow';
import { DSL, IFlow } from '@/interfaces/database/flow';
import { IDebugSingleRequestBody } from '@/interfaces/request/flow';
import i18n from '@/locales/config';
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
import { BeginId } from '@/pages/flow/constant';
import flowService from '@/services/flow-service';
import { buildMessageListWithUuid } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import { set } from 'lodash';
import get from 'lodash/get';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { v4 as uuid } from 'uuid';
export const EmptyDsl = {
graph: {
nodes: [
{
id: BeginId,
type: 'beginNode',
position: {
x: 50,
y: 200,
},
data: {
label: 'Begin',
name: 'begin',
},
sourcePosition: 'left',
targetPosition: 'right',
},
],
edges: [],
},
components: {
begin: {
obj: {
component_name: 'Begin',
params: {},
},
downstream: ['Answer:China'], // other edge target is downstream, edge source is current node id
upstream: [], // edge source is upstream, edge target is current node id
},
},
messages: [],
reference: [],
history: [],
path: [],
answer: [],
};
export const useFetchFlowTemplates = (): ResponseType<IFlowTemplate[]> => {
const { t } = useTranslation();
const { data } = useQuery({
queryKey: ['fetchFlowTemplates'],
initialData: [],
queryFn: async () => {
const { data } = await flowService.listTemplates();
if (Array.isArray(data?.data)) {
data.data.unshift({
id: uuid(),
title: t('flow.blank'),
description: t('flow.createFromNothing'),
dsl: EmptyDsl,
});
}
return data;
},
});
return data;
};
export const useFetchFlowList = (): { data: IFlow[]; loading: boolean } => {
const { data, isFetching: loading } = useQuery({

View File

@ -1,18 +0,0 @@
.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);
}
}

View File

@ -1,107 +0,0 @@
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 };
};

View File

@ -1,31 +0,0 @@
.edgeButton {
width: 14px;
height: 14px;
background: #eee;
border: 1px solid #fff;
padding: 0;
cursor: pointer;
border-radius: 50%;
font-size: 10px;
line-height: 1;
}
.edgeButton:hover {
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08);
}
.edgeButtonDark {
width: 14px;
height: 14px;
background: #0e0c0c;
border: 1px solid #fff;
padding: 0;
cursor: pointer;
border-radius: 50%;
font-size: 10px;
line-height: 1;
}
.edgeButtonDark:hover {
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08);
}

View File

@ -1,108 +0,0 @@
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
} from '@xyflow/react';
import useGraphStore from '../../store';
import { useTheme } from '@/components/theme-provider';
import { useFetchFlow } from '@/hooks/flow-hooks';
import { useMemo } from 'react';
import styles from './index.less';
export function ButtonEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
source,
target,
style = {},
markerEnd,
selected,
}: EdgeProps) {
const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const { theme } = useTheme();
const selectedStyle = useMemo(() => {
return selected ? { strokeWidth: 2, stroke: '#1677ff' } : {};
}, [selected]);
const onEdgeClick = () => {
deleteEdgeById(id);
};
// highlight the nodes that the workflow passes through
const { data: flowDetail } = useFetchFlow();
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 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: 2, stroke: 'red' };
}
}
return {};
}, [source, target, graphPath]);
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{ ...style, ...selectedStyle, ...highlightStyle }}
/>
<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={
theme === 'dark' ? styles.edgeButtonDark : styles.edgeButton
}
type="button"
onClick={onEdgeClick}
>
×
</button>
</div>
</EdgeLabelRenderer>
</>
);
}

View File

@ -1,10 +0,0 @@
.canvasWrapper {
position: relative;
height: 100%;
:global(.react-flow__node-group) {
.commonNode();
padding: 0;
border: 0;
background-color: transparent;
}
}

View File

@ -1,237 +0,0 @@
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
Background,
ConnectionMode,
ControlButton,
Controls,
NodeTypes,
ReactFlow,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { Book, FolderInput, FolderOutput } from 'lucide-react';
import ChatDrawer from '../chat/drawer';
import FormDrawer from '../flow-drawer';
import {
useHandleDrop,
useSelectCanvasData,
useValidateConnection,
useWatchNodeFormDataChange,
} from '../hooks';
import { useBeforeDelete } from '../hooks/use-before-delete';
import { useHandleExportOrImportJsonFile } from '../hooks/use-export-json';
import { useOpenDocument } from '../hooks/use-open-document';
import { useShowDrawer } from '../hooks/use-show-drawer';
import JsonUploadModal from '../json-upload-modal';
import RunDrawer from '../run-drawer';
import { ButtonEdge } from './edge';
import styles from './index.less';
import { RagNode } from './node';
import { BeginNode } from './node/begin-node';
import { CategorizeNode } from './node/categorize-node';
import { EmailNode } from './node/email-node';
import { GenerateNode } from './node/generate-node';
import { InvokeNode } from './node/invoke-node';
import { IterationNode, IterationStartNode } from './node/iteration-node';
import { KeywordNode } from './node/keyword-node';
import { 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';
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,
};
export const edgeTypes = {
buttonEdge: ButtonEdge,
};
interface IProps {
drawerVisible: boolean;
hideDrawer(): void;
}
function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
const {
nodes,
edges,
onConnect,
onEdgesChange,
onNodesChange,
onSelectionChange,
} = useSelectCanvasData();
const isValidConnection = useValidateConnection();
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();
const {
handleExportJson,
handleImportJson,
fileUploadVisible,
onFileUploadOk,
hideFileUploadModal,
} = useHandleExportOrImportJsonFile();
const openDocument = useOpenDocument();
const {
onNodeClick,
onPaneClick,
clickedNode,
formDrawerVisible,
hideFormDrawer,
singleDebugDrawerVisible,
hideSingleDebugDrawer,
showSingleDebugDrawer,
chatVisible,
runVisible,
hideRunOrChatDrawer,
showChatModal,
} = useShowDrawer({
drawerVisible,
hideDrawer,
});
const { handleBeforeDelete } = useBeforeDelete();
useWatchNodeFormDataChange();
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>
<ReactFlow
connectionMode={ConnectionMode.Loose}
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
onEdgesChange={onEdgesChange}
fitView
onConnect={onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
onInit={setReactFlowInstance}
onSelectionChange={onSelectionChange}
nodeOrigin={[0.5, 0]}
isValidConnection={isValidConnection}
defaultEdgeOptions={{
type: 'buttonEdge',
markerEnd: 'logo',
style: {
strokeWidth: 2,
stroke: 'rgb(202 197 245)',
},
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
}}
deleteKeyCode={['Delete', 'Backspace']}
onBeforeDelete={handleBeforeDelete}
>
<Background />
<Controls className="text-black !flex-col-reverse">
<ControlButton onClick={handleImportJson}>
<Tooltip>
<TooltipTrigger asChild>
<FolderInput className="!fill-none" />
</TooltipTrigger>
<TooltipContent>Import</TooltipContent>
</Tooltip>
</ControlButton>
<ControlButton onClick={handleExportJson}>
<Tooltip>
<TooltipTrigger asChild>
<FolderOutput className="!fill-none" />
</TooltipTrigger>
<TooltipContent>Export</TooltipContent>
</Tooltip>
</ControlButton>
<ControlButton onClick={openDocument}>
<Tooltip>
<TooltipTrigger asChild>
<Book className="!fill-none" />
</TooltipTrigger>
<TooltipContent>Document</TooltipContent>
</Tooltip>
</ControlButton>
</Controls>
</ReactFlow>
{formDrawerVisible && (
<FormDrawer
node={clickedNode}
visible={formDrawerVisible}
hideModal={hideFormDrawer}
singleDebugDrawerVisible={singleDebugDrawerVisible}
hideSingleDebugDrawer={hideSingleDebugDrawer}
showSingleDebugDrawer={showSingleDebugDrawer}
></FormDrawer>
)}
{chatVisible && (
<ChatDrawer
visible={chatVisible}
hideModal={hideRunOrChatDrawer}
></ChatDrawer>
)}
{runVisible && (
<RunDrawer
hideModal={hideRunOrChatDrawer}
showModal={showChatModal}
></RunDrawer>
)}
{fileUploadVisible && (
<JsonUploadModal
onOk={onFileUploadOk}
visible={fileUploadVisible}
hideModal={hideFileUploadModal}
></JsonUploadModal>
)}
</div>
);
}
export default FlowCanvas;

View File

@ -1,72 +0,0 @@
import { useTheme } from '@/components/theme-provider';
import { IBeginNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Flex } from 'antd';
import classNames from 'classnames';
import get from 'lodash/get';
import { useTranslation } from 'react-i18next';
import {
BeginQueryType,
BeginQueryTypeIconMap,
Operator,
operatorMap,
} from '../../constant';
import { BeginQuery } from '../../interface';
import OperatorIcon from '../../operator-icon';
import { RightHandleStyle } from './handle-icon';
import styles from './index.less';
// TODO: do not allow other nodes to connect to this node
export function BeginNode({ selected, data }: NodeProps<IBeginNode>) {
const { t } = useTranslation();
const query: BeginQuery[] = get(data, 'form.query', []);
const { theme } = useTheme();
return (
<section
className={classNames(
styles.ragNode,
theme === 'dark' ? styles.dark : '',
{
[styles.selectedNode]: selected,
},
)}
>
<Handle
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
style={RightHandleStyle}
></Handle>
<Flex align="center" justify={'center'} gap={10}>
<OperatorIcon
name={data.label as Operator}
fontSize={24}
color={operatorMap[data.label as Operator].color}
></OperatorIcon>
<div className="truncate text-center font-semibold text-sm">
{t(`flow.begin`)}
</div>
</Flex>
<Flex gap={8} vertical className={styles.generateParameters}>
{query.map((x, idx) => {
const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType];
return (
<Flex
key={idx}
align="center"
gap={6}
className={styles.conditionBlock}
>
<Icon className="size-4" />
<label htmlFor="">{x.key}</label>
<span className={styles.parameterValue}>{x.name}</span>
<span className="flex-1">{x.optional ? 'Yes' : 'No'}</span>
</Flex>
);
})}
</Flex>
</section>
);
}

View File

@ -1,57 +0,0 @@
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>
);
}

View File

@ -1,40 +0,0 @@
import { Handle, Position } from '@xyflow/react';
import React from 'react';
import styles from './index.less';
const DEFAULT_HANDLE_STYLE = {
width: 6,
height: 6,
bottom: -5,
fontSize: 8,
};
interface IProps extends React.PropsWithChildren {
top: number;
right: number;
id: string;
idx?: number;
}
const CategorizeHandle = ({ top, right, id, children }: IProps) => {
return (
<Handle
type="source"
position={Position.Right}
id={id}
isConnectable
style={{
...DEFAULT_HANDLE_STYLE,
top: `${top}%`,
right: `${right}%`,
background: 'red',
color: 'black',
}}
>
<span className={styles.categorizeAnchorPointText}>{children || id}</span>
</Handle>
);
};
export default CategorizeHandle;

View File

@ -1,68 +0,0 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { useTheme } from '@/components/theme-provider';
import { ICategorizeNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { RightHandleStyle } from './handle-icon';
import { useBuildCategorizeHandlePositions } from './hooks';
import styles from './index.less';
import NodeHeader from './node-header';
export function CategorizeNode({
id,
data,
selected,
}: NodeProps<ICategorizeNode>) {
const { positions } = useBuildCategorizeHandlePositions({ data, id });
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>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<Flex vertical gap={8}>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
{positions.map((position, idx) => {
return (
<div key={idx}>
<div className={styles.nodeText}>{position.text}</div>
<Handle
key={position.text}
id={position.text}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
style={{ ...RightHandleStyle, top: position.top }}
></Handle>
</div>
);
})}
</Flex>
</section>
);
}

View File

@ -1,58 +0,0 @@
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;

View File

@ -1,78 +0,0 @@
import { IEmailNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Flex } from 'antd';
import classNames from 'classnames';
import { useState } from 'react';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function EmailNode({
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>
);
}

View File

@ -1,57 +0,0 @@
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 { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function GenerateNode({
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>
);
}

View File

@ -1,20 +0,0 @@
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;

View File

@ -1,104 +0,0 @@
import { useUpdateNodeInternals } from '@xyflow/react';
import get from 'lodash/get';
import { useEffect, useMemo } from 'react';
import { SwitchElseTo } from '../../constant';
import {
ICategorizeItemResult,
ISwitchCondition,
RAGFlowNodeType,
} from '@/interfaces/database/flow';
import { generateSwitchHandleText } from '../../utils';
export const useBuildCategorizeHandlePositions = ({
data,
id,
}: {
id: string;
data: RAGFlowNodeType['data'];
}) => {
const updateNodeInternals = useUpdateNodeInternals();
const categoryData: ICategorizeItemResult = useMemo(() => {
return get(data, `form.category_description`, {});
}, [data]);
const positions = useMemo(() => {
const list: Array<{
text: string;
top: number;
idx: number;
}> = [];
Object.keys(categoryData)
.sort((a, b) => categoryData[a].index - categoryData[b].index)
.forEach((x, idx) => {
list.push({
text: x,
idx,
top: idx === 0 ? 98 + 20 : list[idx - 1].top + 8 + 26,
});
});
return list;
}, [categoryData]);
useEffect(() => {
updateNodeInternals(id);
}, [id, updateNodeInternals, categoryData]);
return { positions };
};
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 ? 58 + 20 : list[idx - 1].top + 32; // case number (Case 1) height + flex gap
if (idx - 1 >= 0) {
const previousItems = conditions[idx - 1]?.items ?? [];
if (previousItems.length > 0) {
top += 12; // ConditionBlock padding
top += previousItems.length * 22; // 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 };
};

View File

@ -1,285 +0,0 @@
.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);
}
}

View File

@ -1,45 +0,0 @@
import { useTheme } from '@/components/theme-provider';
import { IRagNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function RagNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IRagNode>) {
const { theme } = useTheme();
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}></NodeHeader>
</section>
);
}

View File

@ -1,59 +0,0 @@
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 { useTranslation } from 'react-i18next';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function InvokeNode({
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>
);
}

View File

@ -1,127 +0,0 @@
import { useTheme } from '@/components/theme-provider';
import {
IIterationNode,
IIterationStartNode,
} from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { Handle, NodeProps, NodeResizeControl, Position } from '@xyflow/react';
import { ListRestart } from 'lucide-react';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
function ResizeIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="#5025f9"
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>
);
}
const controlStyle = {
background: 'transparent',
border: 'none',
cursor: 'nwse-resize',
};
export function IterationNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IIterationNode>) {
const { theme } = useTheme();
return (
<section
className={cn(
'w-full h-full bg-zinc-200 opacity-70',
styles.iterationNode,
{
['bg-gray-800']: theme === 'dark',
[styles.selectedIterationNode]: selected,
},
)}
>
<NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}>
<ResizeIcon />
</NodeResizeControl>
<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}
wrapperClassName={cn(
'p-2 bg-white rounded-t-[10px] absolute w-full top-[-60px] left-[-0.3px]',
styles.iterationHeader,
{
[`${styles.dark} text-white`]: theme === 'dark',
[styles.selectedHeader]: selected,
},
)}
></NodeHeader>
</section>
);
}
export function IterationStartNode({
isConnectable = true,
selected,
}: NodeProps<IIterationStartNode>) {
const { theme } = useTheme();
return (
<section
className={cn('bg-white p-2 rounded-xl', {
[styles.dark]: theme === 'dark',
[styles.selectedNode]: selected,
})}
>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
isConnectableEnd={false}
></Handle>
<div>
<ListRestart className="size-7" />
</div>
</section>
);
}

View File

@ -1,57 +0,0 @@
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 { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function KeywordNode({
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>
);
}

View File

@ -1,45 +0,0 @@
import { useTheme } from '@/components/theme-provider';
import { ILogicNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function LogicNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<ILogicNode>) {
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}></NodeHeader>
</section>
);
}

View File

@ -1,65 +0,0 @@
import { useTheme } from '@/components/theme-provider';
import { IMessageNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function MessageNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IMessageNode>) {
const messages: string[] = get(data, 'form.messages', []);
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={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>
</section>
);
}

View File

@ -1,73 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Flex } from 'antd';
import { Play } from 'lucide-react';
import { Operator, operatorMap } from '../../constant';
import OperatorIcon from '../../operator-icon';
import { needsSingleStepDebugging } from '../../utils';
import NodeDropdown from './dropdown';
import { NextNodePopover } from './popover';
import { RunTooltip } from '../../flow-tooltip';
interface IProps {
id: string;
label: string;
name: string;
gap?: number;
className?: string;
wrapperClassName?: string;
}
const ExcludedRunStateOperators = [Operator.Answer];
export function RunStatus({ id, name, label }: IProps) {
const { t } = useTranslate('flow');
return (
<section className="flex justify-end items-center pb-1 gap-2 text-blue-600">
{needsSingleStepDebugging(label) && (
<RunTooltip>
<Play className="size-3 cursor-pointer" data-play />
</RunTooltip> // data-play is used to trigger single step debugging
)}
<NextNodePopover nodeId={id} name={name}>
<span className="cursor-pointer text-[10px]">
{t('operationResults')}
</span>
</NextNodePopover>
</section>
);
}
const NodeHeader = ({
label,
id,
name,
gap = 4,
className,
wrapperClassName,
}: IProps) => {
return (
<section className={wrapperClassName}>
{!ExcludedRunStateOperators.includes(label as Operator) && (
<RunStatus id={id} name={name} label={label}></RunStatus>
)}
<Flex
flex={1}
align="center"
justify={'space-between'}
gap={gap}
className={className}
>
<OperatorIcon
name={label as Operator}
color={operatorMap[label as Operator]?.color}
></OperatorIcon>
<span className="truncate text-center font-semibold text-sm">
{name}
</span>
<NodeDropdown id={id} label={label}></NodeDropdown>
</Flex>
</section>
);
};
export default NodeHeader;

View File

@ -1,92 +0,0 @@
import { NodeProps, NodeResizeControl } from '@xyflow/react';
import { Flex, Form, Input } from 'antd';
import classNames from 'classnames';
import NodeDropdown from './dropdown';
import SvgIcon from '@/components/svg-icon';
import { useTheme } from '@/components/theme-provider';
import { INoteNode } from '@/interfaces/database/flow';
import { memo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
useHandleFormValuesChange,
useHandleNodeNameChange,
} from '../../hooks';
import styles from './index.less';
const { TextArea } = Input;
const controlStyle = {
background: 'transparent',
border: 'none',
};
function NoteNode({ data, id }: NodeProps<INoteNode>) {
const { t } = useTranslation();
const [form] = Form.useForm();
const { theme } = useTheme();
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
id,
data,
});
const { handleValuesChange } = useHandleFormValuesChange(id);
useEffect(() => {
form.setFieldsValue(data?.form);
}, [form, data?.form]);
return (
<>
<NodeResizeControl style={controlStyle} minWidth={190} minHeight={128}>
<SvgIcon
name="resize"
width={12}
style={{
position: 'absolute',
right: 5,
bottom: 5,
cursor: 'nwse-resize',
}}
></SvgIcon>
</NodeResizeControl>
<section
className={classNames(
styles.noteNode,
theme === 'dark' ? styles.dark : '',
)}
>
<Flex
justify={'space-between'}
className={classNames('note-drag-handle')}
align="center"
gap={6}
>
<SvgIcon name="note" width={14}></SvgIcon>
<Input
value={name ?? t('flow.note')}
onBlur={handleNameBlur}
onChange={handleNameChange}
className={styles.noteName}
></Input>
<NodeDropdown id={id} label={data.label}></NodeDropdown>
</Flex>
<Form
onValuesChange={handleValuesChange}
form={form}
className={styles.noteForm}
>
<Form.Item name="text" noStyle>
<TextArea
rows={3}
placeholder={t('flow.notePlaceholder')}
className={styles.noteTextarea}
/>
</Form.Item>
</Form>
</section>
</>
);
}
export default memo(NoteNode);

View File

@ -1,313 +0,0 @@
import { useFetchFlow } from '@/hooks/flow-hooks';
import get from 'lodash/get';
import React, {
MouseEventHandler,
useCallback,
useMemo,
useState,
} from 'react';
import JsonView from 'react18-json-view';
import 'react18-json-view/src/style.css';
import { useReplaceIdWithText } from '../../hooks';
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 {
Button,
Card,
Col,
Input,
Row,
Space,
Tabs,
Typography,
message,
} from 'antd';
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 } = useFetchFlow();
console.log(data);
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 { conf, messages, prompt } = get(
component,
['obj', 'params', 'infor'],
{},
);
const { replacedOutput } = useReplaceIdWithText(output);
const stopPropagation: MouseEventHandler = useCallback((e) => {
e.stopPropagation();
}, []);
const getLabel = useGetComponentLabelByValue(nodeId);
const [inputPage, setInputPage] = useState(1);
const pageSize = 3;
const pagedInputs = inputs.slice(
(inputPage - 1) * pageSize,
inputPage * pageSize,
);
return (
<Popover>
<PopoverTrigger onClick={stopPropagation} asChild>
{children}
</PopoverTrigger>
<PopoverContent
align={'start'}
side={'right'}
sideOffset={20}
onClick={stopPropagation}
className="w-[800px] p-4"
style={{ maxHeight: 600, overflow: 'auto' }}
>
<Card
bordered={false}
style={{ marginBottom: 16, padding: 0 }}
bodyStyle={{ padding: 0 }}
>
<Typography.Title
level={5}
style={{
marginBottom: 16,
fontWeight: 600,
fontSize: 18,
borderBottom: '1px solid #f0f0f0',
paddingBottom: 8,
}}
>
{name} {t('operationResults')}
</Typography.Title>
</Card>
<Tabs
defaultActiveKey="input"
items={[
{
key: 'input',
label: t('input'),
children: (
<Card
size="small"
className="bg-gray-50 dark:bg-gray-800"
style={{ borderRadius: 8, border: '1px solid #e5e7eb' }}
bodyStyle={{ padding: 16 }}
>
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('componentId')}</TableHead>
<TableHead className="w-[60px]">
{t('content')}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pagedInputs.map((x, idx) => (
<TableRow key={idx + (inputPage - 1) * pageSize}>
<TableCell>{getLabel(x.component_id)}</TableCell>
<TableCell className="truncate">
{x.content}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* Pagination */}
{inputs.length > pageSize && (
<Row justify="end" style={{ marginTop: 8 }}>
<Space>
<Button
size="small"
disabled={inputPage === 1}
onClick={() => setInputPage(inputPage - 1)}
>
Prev
</Button>
<span className="mx-2 text-sm">
{inputPage} / {Math.ceil(inputs.length / pageSize)}
</span>
<Button
size="small"
disabled={
inputPage === Math.ceil(inputs.length / pageSize)
}
onClick={() => setInputPage(inputPage + 1)}
>
Next
</Button>
</Space>
</Row>
)}
</Card>
),
},
{
key: 'output',
label: t('output'),
children: (
<Card
size="small"
className="bg-gray-50 dark:bg-gray-800"
style={{ borderRadius: 8, border: '1px solid #e5e7eb' }}
bodyStyle={{ padding: 16 }}
>
<JsonView
src={replacedOutput}
displaySize={30}
className="w-full max-h-[300px] break-words overflow-auto"
/>
</Card>
),
},
{
key: 'infor',
label: t('infor'),
children: (
<Card
size="small"
className="bg-gray-50 dark:bg-gray-800"
style={{ borderRadius: 8, border: '1px solid #e5e7eb' }}
bodyStyle={{ padding: 16 }}
>
<Row gutter={16}>
<Col span={12}>
{conf && (
<Card
size="small"
bordered={false}
style={{
marginBottom: 16,
background: 'transparent',
}}
bodyStyle={{ padding: 0 }}
>
<Typography.Text
strong
style={{
color: '#888',
marginBottom: 8,
display: 'block',
}}
>
Configuration:
</Typography.Text>
<JsonView
src={conf}
displaySize={30}
className="w-full max-h-[120px] break-words overflow-auto"
/>
</Card>
)}
{prompt && (
<Card
size="small"
bordered={false}
style={{ background: 'transparent' }}
bodyStyle={{ padding: 0 }}
>
<Row
align="middle"
justify="space-between"
style={{ marginBottom: 8 }}
>
<Col>
<Typography.Text strong style={{ color: '#888' }}>
Prompt:
</Typography.Text>
</Col>
<Col>
<Button
size="small"
onClick={() => {
const inlineString = prompt
.replace(/\s+/g, ' ')
.trim();
navigator.clipboard.writeText(inlineString);
message.success(
'Prompt copied as single line!',
);
}}
>
Copy as single line
</Button>
</Col>
</Row>
<Input.TextArea
value={prompt}
readOnly
autoSize={{ minRows: 2, maxRows: 6 }}
className="bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700"
/>
</Card>
)}
</Col>
<Col span={12}>
{messages && (
<Card
size="small"
bordered={false}
style={{
marginBottom: 16,
background: 'transparent',
}}
bodyStyle={{ padding: 0 }}
>
<Typography.Text
strong
style={{
color: '#888',
marginBottom: 8,
display: 'block',
}}
>
Messages:
</Typography.Text>
<div className="max-h-[300px] overflow-auto">
<JsonView
src={messages}
displaySize={30}
className="w-full break-words"
/>
</div>
</Card>
)}
</Col>
</Row>
</Card>
),
},
]}
/>
</PopoverContent>
</Popover>
);
}

View File

@ -1,70 +0,0 @@
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 { useReplaceIdWithName } from '../../hooks';
import styles from './index.less';
import NodeHeader from './node-header';
export function RelevantNode({ 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>
);
}

View File

@ -1,115 +0,0 @@
import { useTheme } from '@/components/theme-provider';
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
import { IRetrievalNode } from '@/interfaces/database/flow';
import { UserOutlined } from '@ant-design/icons';
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Avatar, Button, Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { useMemo, useState } from 'react';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function RetrievalNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IRetrievalNode>) {
const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []);
const { theme } = useTheme();
const { list: knowledgeList } = useFetchKnowledgeList(true);
const [showAllKnowledge, setShowAllKnowledge] = useState(false);
const knowledgeBases = useMemo(() => {
return knowledgeBaseIds.map((x) => {
const item = knowledgeList.find((y) => x === y.id);
return {
name: item?.name,
avatar: item?.avatar,
id: x,
};
});
}, [knowledgeList, knowledgeBaseIds]);
const displayedKnowledgeBases = showAllKnowledge
? knowledgeBases
: knowledgeBases.slice(0, 3);
function showAllItem(e: any) {
e.stopPropagation(); // Prevent event from bubbling to parent
setShowAllKnowledge(!showAllKnowledge);
}
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={classNames({
[styles.nodeHeader]: knowledgeBaseIds.length > 0,
})}
></NodeHeader>
<Flex vertical gap={8}>
{displayedKnowledgeBases.map((knowledge) => {
return (
<div className={styles.nodeText} key={knowledge.id}>
<Flex align={'center'} gap={6}>
<Avatar
size={26}
icon={<UserOutlined />}
src={knowledge.avatar}
/>
<Flex className={styles.knowledgeNodeName} flex={1}>
{knowledge.name}
</Flex>
</Flex>
</div>
);
})}
{knowledgeBases.length > 3 && (
<div className={styles.nodeText}>
<Button
type="link"
size="small"
onClick={showAllItem}
style={{
padding: 0,
height: 'auto',
lineHeight: '1.5',
textAlign: 'left',
}}
>
{showAllKnowledge
? 'Hide'
: `Show more ${knowledgeBases.length - 3} knowledge`}
</Button>
</div>
)}
</Flex>
</section>
);
}

View File

@ -1,57 +0,0 @@
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 { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function RewriteNode({
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>
);
}

View File

@ -1,114 +0,0 @@
import { useTheme } from '@/components/theme-provider';
import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Divider, Flex } from 'antd';
import classNames from 'classnames';
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
import { RightHandleStyle } from './handle-icon';
import { useBuildSwitchHandlePositions } from './hooks';
import styles from './index.less';
import NodeHeader from './node-header';
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 = useGetComponentLabelByValue(nodeId);
return (
<Flex vertical className={styles.conditionBlock}>
{items.map((x, idx) => (
<div key={idx}>
<Flex>
<div
className={classNames(styles.conditionLine, styles.conditionKey)}
>
{getLabel(x?.cpn_id)}
</div>
<span className={styles.conditionOperator}>{x?.operator}</span>
<Flex flex={1} className={styles.conditionLine}>
{x?.value}
</Flex>
</Flex>
{idx + 1 < items.length && (
<Divider orientationMargin="0" className={styles.zeroDivider}>
{condition?.logical_operator}
</Divider>
)}
</div>
))}
</Flex>
);
};
export function SwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) {
const { positions } = useBuildSwitchHandlePositions({ data, id });
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>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<Flex vertical gap={10}>
{positions.map((position, idx) => {
return (
<div key={idx}>
<Flex vertical>
<Flex justify={'space-between'}>
<span>{idx < positions.length - 1 && position.text}</span>
<span>{getConditionKey(idx, positions.length)}</span>
</Flex>
{position.condition && (
<ConditionBlock
nodeId={id}
condition={position.condition}
></ConditionBlock>
)}
</Flex>
<Handle
key={position.text}
id={position.text}
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
style={{ ...RightHandleStyle, top: position.top }}
></Handle>
</div>
);
})}
</Flex>
</section>
);
}

View File

@ -1,75 +0,0 @@
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 styles from './index.less';
export function TemplateNode({
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>
);
}

View File

@ -1,92 +0,0 @@
import MessageItem from '@/components/message-item';
import { MessageType } from '@/constants/chat';
import { useGetFileIcon } from '@/pages/chat/hooks';
import { buildMessageItemReference } from '@/pages/chat/utils';
import { Flex, Spin } from 'antd';
import { useSendNextMessage } from './hooks';
import MessageInput from '@/components/message-input';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { useFetchFlow } from '@/hooks/flow-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import styles from './index.less';
const FlowChatBox = () => {
const {
sendLoading,
handleInputChange,
handlePressEnter,
value,
loading,
ref,
derivedMessages,
reference,
stopOutputMessage,
} = useSendNextMessage();
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
useGetFileIcon();
const { data: userInfo } = useFetchUserInfo();
const { data: canvasInfo } = useFetchFlow();
return (
<>
<Flex flex={1} className={styles.chatContainer} vertical>
<Flex flex={1} vertical className={styles.messageContainer}>
<div>
<Spin spinning={loading}>
{derivedMessages?.map((message, i) => {
return (
<MessageItem
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages.length - 1 === i
}
key={buildMessageUuidWithRole(message)}
nickname={userInfo.nickname}
avatar={userInfo.avatar}
avatarDialog={canvasInfo.avatar}
item={message}
reference={buildMessageItemReference(
{ message: derivedMessages, reference },
message,
)}
clickDocumentButton={clickDocumentButton}
index={i}
showLikeButton={false}
sendLoading={sendLoading}
></MessageItem>
);
})}
</Spin>
</div>
<div ref={ref} />
</Flex>
<MessageInput
showUploadIcon={false}
value={value}
sendLoading={sendLoading}
disabled={false}
sendDisabled={sendLoading}
conversationId=""
onPressEnter={handlePressEnter}
onInputChange={handleInputChange}
stopOutputMessage={stopOutputMessage}
/>
</Flex>
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
</>
);
};
export default FlowChatBox;

View File

@ -1,25 +0,0 @@
import { useFetchFlow } from '@/hooks/flow-hooks';
import { IModalProps } from '@/interfaces/common';
import { Drawer } from 'antd';
import { getDrawerWidth } from '../utils';
import FlowChatBox from './box';
const ChatDrawer = ({ visible, hideModal }: IModalProps<any>) => {
const { data } = useFetchFlow();
return (
<Drawer
title={data.title}
placement="right"
onClose={hideModal}
open={visible}
getContainer={false}
width={getDrawerWidth()}
mask={false}
>
<FlowChatBox></FlowChatBox>
</Drawer>
);
};
export default ChatDrawer;

View File

@ -1,145 +0,0 @@
import { MessageType } from '@/constants/chat';
import { useFetchFlow } from '@/hooks/flow-hooks';
import {
useHandleMessageInputChange,
useSelectDerivedMessages,
useSendMessageWithSse,
} from '@/hooks/logic-hooks';
import { Message } from '@/interfaces/database/chat';
import i18n from '@/locales/config';
import api from '@/utils/api';
import { message } from 'antd';
import trim from 'lodash/trim';
import { useCallback, useEffect } from 'react';
import { useParams } from 'umi';
import { v4 as uuid } from 'uuid';
import { receiveMessageError } from '../utils';
const antMessage = message;
export const useSelectNextMessages = () => {
const { data: flowDetail, loading } = useFetchFlow();
const reference = flowDetail.dsl.reference;
const {
derivedMessages,
ref,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
} = useSelectDerivedMessages();
return {
reference,
loading,
derivedMessages,
ref,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
};
};
export const useSendNextMessage = () => {
const {
reference,
loading,
derivedMessages,
ref,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
} = useSelectNextMessages();
const { id: flowId } = useParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { refetch } = useFetchFlow();
const { send, answer, done, stopOutputMessage } = useSendMessageWithSse(
api.runCanvas,
);
const sendMessage = useCallback(
async ({ message }: { message: Message; messages?: Message[] }) => {
const params: Record<string, unknown> = {
id: flowId,
};
params.running_hint_text = i18n.t('flow.runningHintText', {
defaultValue: 'is running...🕞',
});
if (message.content) {
params.message = message.content;
params.message_id = message.id;
}
const res = await send(params);
if (receiveMessageError(res)) {
antMessage.error(res?.data?.message);
// cancel loading
setValue(message.content);
removeLatestMessage();
} else {
refetch(); // pull the message list after sending the message successfully
}
},
[flowId, send, setValue, removeLatestMessage, refetch],
);
const handleSendMessage = useCallback(
async (message: Message) => {
sendMessage({ message });
},
[sendMessage],
);
useEffect(() => {
if (answer.answer) {
addNewestAnswer(answer);
}
}, [answer, addNewestAnswer]);
const handlePressEnter = useCallback(() => {
if (trim(value) === '') return;
const id = uuid();
if (done) {
setValue('');
handleSendMessage({ id, content: value.trim(), role: MessageType.User });
}
addNewestQuestion({
content: value,
id,
role: MessageType.User,
});
}, [addNewestQuestion, handleSendMessage, done, setValue, value]);
const fetchPrologue = useCallback(async () => {
// fetch prologue
const sendRet = await send({ id: flowId });
if (receiveMessageError(sendRet)) {
message.error(sendRet?.data?.message);
} else {
refetch();
}
}, [flowId, refetch, send]);
useEffect(() => {
fetchPrologue();
}, [fetchPrologue]);
return {
handlePressEnter,
handleInputChange,
value,
sendLoading: !done,
reference,
loading,
derivedMessages,
ref,
removeMessageById,
stopOutputMessage,
};
};

View File

@ -1,8 +0,0 @@
.chatContainer {
padding: 0;
height: 100%;
.messageContainer {
overflow-y: auto;
padding-right: 24px;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { createContext } from 'react';
export const FlowFormContext = createContext<RAGFlowNodeType | undefined>(
undefined,
);

View File

@ -1,312 +0,0 @@
{
"edges": [
{
"id": "63a2f242-8e71-4098-a46a-459a76d538bd",
"label": "",
"source": "begin",
"target": "answer:0"
},
{
"id": "cc6dd8bb-e9dc-46e8-9009-5b96f98ae6c0",
"label": "",
"source": "generate:casual",
"target": "answer:0"
},
{
"id": "58dbf05a-07fc-4a0a-8c03-5f9117e48c35",
"label": "",
"source": "generate:answer",
"target": "answer:0"
},
{
"id": "dd0ff4f2-4d75-4e7d-a505-3e9533402823",
"label": "",
"source": "generate:complain",
"target": "answer:0"
},
{
"id": "3dc7a511-9cde-4080-a572-6b06a64e0458",
"label": "",
"source": "generate:ask_contact",
"target": "answer:0"
},
{
"id": "20e31fee-c392-4257-860a-5844d264198e",
"label": "",
"source": "message:get_contact",
"target": "answer:0"
},
{
"id": "104ac8bb-5d75-4eca-8065-1d8fc2805b8e",
"label": "",
"source": "answer:0",
"target": "categorize:0"
},
{
"id": "755864b5-9eef-44ec-a560-9a23b5a3f9ee",
"label": "",
"source": "categorize:0",
"target": "retrieval:0",
"sourceHandle": "product_related"
},
{
"id": "7f68384d-3441-4bfa-bf13-69af67e857d2",
"label": "",
"source": "categorize:0",
"target": "generate:casual",
"sourceHandle": "casual"
},
{
"id": "c9bf8e81-9345-4885-b565-be2f5b16f6ef",
"label": "",
"source": "categorize:0",
"target": "generate:complain",
"sourceHandle": "complain"
},
{
"id": "2f326699-621b-4d28-ab98-70d99ad21add",
"label": "",
"source": "categorize:0",
"target": "message:get_contact",
"sourceHandle": "answer"
},
{
"id": "03e45174-55df-47d6-8e5f-fbe2ffc148d7",
"label": "",
"source": "retrieval:0",
"target": "relevant:0"
},
{
"id": "a26027ac-e8a9-48e4-814c-3262b8d81913",
"label": "",
"source": "relevant:0",
"target": "generate:answer"
},
{
"id": "04d99dc7-6120-4169-98c6-7bc2813aa85b",
"label": "",
"source": "relevant:0",
"target": "generate:ask_contact"
}
],
"nodes": [
{
"id": "begin",
"type": "beginNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Begin",
"name": "LegalPoetsAttack",
"form": {
"prologue": "Hi! How can I help you?"
}
},
"sourcePosition": "left",
"targetPosition": "right"
},
{
"id": "answer:0",
"type": "ragNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Answer",
"name": "ThreeGeeseBehave",
"form": {}
},
"sourcePosition": "left",
"targetPosition": "right"
},
{
"id": "categorize:0",
"type": "categorizeNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Categorize",
"name": "PublicComicsHammer",
"form": {
"llm_id": "deepseek-chat",
"category_description": {
"product_related": {
"description": "The question is about the product usage, appearance and how it works.",
"examples": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?\nException: Can't connect to ES cluster\nHow to build the RAGFlow image from scratch",
"to": "retrieval:0"
},
"casual": {
"description": "The question is not about the product usage, appearance and how it works. Just casual chat.",
"examples": "How are you doing?\nWhat is your name?\nAre you a robot?\nWhat's the weather?\nWill it rain?",
"to": "generate:casual"
},
"complain": {
"description": "Complain even curse about the product or service you provide. But the comment is not specific enough.",
"examples": "How bad is it.\nIt's really sucks.\nDamn, for God's sake, can it be more steady?\nShit, I just can't use this shit.\nI can't stand it anymore.",
"to": "generate:complain"
},
"answer": {
"description": "This answer provide a specific contact information, like e-mail, phone number, wechat number, line number, twitter, discord, etc,.",
"examples": "My phone number is 203921\nkevinhu.hk@gmail.com\nThis is my discord number: johndowson_29384",
"to": "message:get_contact"
}
},
"message_history_window_size": 8
}
},
"sourcePosition": "left",
"targetPosition": "right"
},
{
"id": "generate:casual",
"type": "ragNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Generate",
"name": "SourKnivesPay",
"form": {
"llm_id": "deepseek-chat",
"prompt": "You are a customer support. But the customer wants to have a casual chat with you instead of consulting about the product. Be nice, funny, enthusiasm and concern.",
"temperature": 0.9,
"message_history_window_size": 12,
"cite": false
}
},
"sourcePosition": "left",
"targetPosition": "right"
},
{
"id": "generate:complain",
"type": "ragNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Generate",
"name": "TameLlamasSniff",
"form": {
"llm_id": "deepseek-chat",
"prompt": "You are a customer support. the Customers complain even curse about the products but not specific enough. You need to ask him/her what's the specific problem with the product. Be nice, patient and concern to soothe your customers emotions at first place.",
"temperature": 0.9,
"message_history_window_size": 12,
"cite": false
}
},
"sourcePosition": "left",
"targetPosition": "right"
},
{
"id": "retrieval:0",
"type": "ragNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Retrieval",
"name": "ShinyPathsDraw",
"form": {
"similarity_threshold": 0.2,
"keywords_similarity_weight": 0.3,
"top_n": 6,
"top_k": 1024,
"rerank_id": "BAAI/bge-reranker-v2-m3",
"kb_ids": ["869a236818b811ef91dffa163e197198"]
}
},
"sourcePosition": "left",
"targetPosition": "right"
},
{
"id": "relevant:0",
"type": "relevantNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Relevant",
"name": "LegalPotsLick",
"form": {
"llm_id": "deepseek-chat",
"temperature": 0.02,
"yes": "generate:answer",
"no": "generate:ask_contact"
}
},
"sourcePosition": "left",
"targetPosition": "right"
},
{
"id": "generate:answer",
"type": "ragNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Generate",
"name": "YellowGamesReport",
"form": {
"llm_id": "deepseek-chat",
"prompt": "You are an intelligent assistant. Please answer the question based on content of knowledge base. When all knowledge base content is irrelevant to the question, your answer must include the sentence \"The answer you are looking for is not found in the knowledge base!\". Answers need to consider chat history.\n Knowledge base content is as following:\n {input}\n The above is the content of knowledge base.",
"temperature": 0.02
}
},
"sourcePosition": "left",
"targetPosition": "right"
},
{
"id": "generate:ask_contact",
"type": "ragNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Generate",
"name": "FamousChefsRetire",
"form": {
"llm_id": "deepseek-chat",
"prompt": "You are a customer support. But you can't answer to customers' question. You need to request their contact like E-mail, phone number, Wechat number, LINE number, twitter, discord, etc,. Product experts will contact them later. Please do not ask the same question twice.",
"temperature": 0.9,
"message_history_window_size": 12,
"cite": false
}
},
"sourcePosition": "left",
"targetPosition": "right"
},
{
"id": "message:get_contact",
"type": "ragNode",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Message",
"name": "BlueBooksTan",
"form": {
"messages": [
"Okay, I've already write this down. What else I can do for you?",
"Get it. What else I can do for you?",
"Thanks for your trust! Our expert will contact ASAP. So, anything else I can do for you?",
"Thanks! So, anything else I can do for you?"
]
}
},
"sourcePosition": "left",
"targetPosition": "right"
}
]
}

View File

@ -1,5 +0,0 @@
.formWrapper {
:global(.ant-form-item-label) {
font-weight: 600 !important;
}
}

View File

@ -1,250 +0,0 @@
import { Authorization } from '@/constants/authorization';
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { useHandleSubmittable } from '@/hooks/login-hooks';
import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util';
import { UploadOutlined } from '@ant-design/icons';
import {
Button,
Form,
FormItemProps,
Input,
InputNumber,
Select,
Switch,
Upload,
} from 'antd';
import { UploadChangeParam, UploadFile } from 'antd/es/upload';
import { pick } from 'lodash';
import { Link } from 'lucide-react';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { BeginQueryType } from '../constant';
import { BeginQuery } from '../interface';
import { PopoverForm } from './popover-form';
import styles from './index.less';
import KnowledgeBaseItem from '@/components/knowledge-base-item';
interface IProps {
parameters: BeginQuery[];
ok(parameters: any[]): void;
isNext?: boolean;
loading?: boolean;
submitButtonDisabled?: boolean;
}
const DebugContent = ({
parameters,
ok,
isNext = true,
loading = false,
submitButtonDisabled = false,
}: IProps) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const {
visible,
hideModal: hidePopover,
switchVisible,
showModal: showPopover,
} = useSetModalState();
const { setRecord, currentRecord } = useSetSelectedRecord<number>();
const { submittable } = useHandleSubmittable(form);
const [isUploading, setIsUploading] = useState(false);
const handleShowPopover = useCallback(
(idx: number) => () => {
setRecord(idx);
showPopover();
},
[setRecord, showPopover],
);
const normFile = (e: any) => {
if (Array.isArray(e)) {
return e;
}
return e?.fileList;
};
const onChange = useCallback(
(optional: boolean) =>
({ fileList }: UploadChangeParam<UploadFile>) => {
if (!optional) {
setIsUploading(fileList.some((x) => x.status === 'uploading'));
}
},
[],
);
const renderWidget = useCallback(
(q: BeginQuery, idx: number) => {
const props: FormItemProps & { key: number } = {
key: idx,
label: q.name ?? q.key,
name: idx,
};
if (q.optional === false) {
props.rules = [{ required: true }];
}
const urlList: { url: string; result: string }[] =
form.getFieldValue(idx) || [];
const BeginQueryTypeMap = {
[BeginQueryType.Line]: (
<Form.Item {...props}>
<Input></Input>
</Form.Item>
),
[BeginQueryType.Paragraph]: (
<Form.Item {...props}>
<Input.TextArea rows={1}></Input.TextArea>
</Form.Item>
),
[BeginQueryType.Options]: (
<Form.Item {...props}>
<Select
allowClear
options={q.options?.map((x) => ({ label: x, value: x })) ?? []}
></Select>
</Form.Item>
),
[BeginQueryType.File]: (
<React.Fragment key={idx}>
<Form.Item label={q.name ?? q.key} required={!q.optional}>
<div className="relative">
<Form.Item
{...props}
valuePropName="fileList"
getValueFromEvent={normFile}
noStyle
>
<Upload
name="file"
action={api.parse}
multiple
headers={{ [Authorization]: getAuthorization() }}
onChange={onChange(q.optional)}
>
<Button icon={<UploadOutlined />}>
{t('common.upload')}
</Button>
</Upload>
</Form.Item>
<Form.Item
{...pick(props, ['key', 'label', 'rules'])}
required={!q.optional}
className={urlList.length > 0 ? 'mb-1' : ''}
noStyle
>
<PopoverForm visible={visible} switchVisible={switchVisible}>
<Button
onClick={handleShowPopover(idx)}
className="absolute left-1/2 top-0"
icon={<Link className="size-3" />}
>
{t('flow.pasteFileLink')}
</Button>
</PopoverForm>
</Form.Item>
</div>
</Form.Item>
<Form.Item name={idx} noStyle {...pick(props, ['rules'])} />
</React.Fragment>
),
[BeginQueryType.Integer]: (
<Form.Item {...props}>
<InputNumber></InputNumber>
</Form.Item>
),
[BeginQueryType.Boolean]: (
<Form.Item valuePropName={'checked'} {...props}>
<Switch></Switch>
</Form.Item>
),
[BeginQueryType.KnowledgeBases]: (
<KnowledgeBaseItem
name={idx.toString()}
label={q.name || q.key}
required={!q.optional}
></KnowledgeBaseItem>
)
};
return (
BeginQueryTypeMap[q.type as BeginQueryType] ??
BeginQueryTypeMap[BeginQueryType.Paragraph]
);
},
[form, handleShowPopover, onChange, switchVisible, t, visible],
);
const onOk = useCallback(async () => {
const values = await form.validateFields();
const nextValues = Object.entries(values).map(([key, value]) => {
const item = parameters[Number(key)];
let nextValue = value;
if (Array.isArray(value)) {
nextValue = ``;
if (item.type === 'kb') {
nextValue = value.join(',')
} else {
value.forEach((x) => {
nextValue +=
x?.originFileObj instanceof File
? `${x.name}\n${x.response?.data}\n----\n`
: `${x.url}\n${x.result}\n----\n`;
});
}
}
return { ...item, value: nextValue };
});
ok(nextValues);
}, [form, ok, parameters]);
return (
<>
<section className={styles.formWrapper}>
<Form.Provider
onFormFinish={(name, { values, forms }) => {
if (name === 'urlForm') {
const { basicForm } = forms;
const urlInfo = basicForm.getFieldValue(currentRecord) || [];
basicForm.setFieldsValue({
[currentRecord]: [...urlInfo, { ...values, name: values.url }],
});
hidePopover();
}
}}
>
<Form
name="basicForm"
autoComplete="off"
layout={'vertical'}
form={form}
>
{parameters.map((x, idx) => {
return renderWidget(x, idx);
})}
</Form>
</Form.Provider>
</section>
<Button
type={'primary'}
block
onClick={onOk}
loading={loading}
disabled={!submittable || isUploading || submitButtonDisabled}
>
{t(isNext ? 'common.next' : 'flow.run')}
</Button>
</>
);
};
export default DebugContent;

View File

@ -1,74 +0,0 @@
import { useParseDocument } from '@/hooks/document-hooks';
import { useResetFormOnCloseModal } from '@/hooks/logic-hooks';
import { IModalProps } from '@/interfaces/common';
import { Button, Form, Input, Popover } from 'antd';
import { PropsWithChildren } from 'react';
import { useTranslation } from 'react-i18next';
const reg =
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/;
export const PopoverForm = ({
children,
visible,
switchVisible,
}: PropsWithChildren<IModalProps<any>>) => {
const [form] = Form.useForm();
const { parseDocument, loading } = useParseDocument();
const { t } = useTranslation();
useResetFormOnCloseModal({
form,
visible,
});
const onOk = async () => {
const values = await form.validateFields();
const val = values.url;
if (reg.test(val)) {
const ret = await parseDocument(val);
if (ret?.data?.code === 0) {
form.setFieldValue('result', ret?.data?.data);
form.submit();
}
}
};
const content = (
<Form form={form} name="urlForm">
<Form.Item
name="url"
rules={[{ required: true, type: 'url' }]}
className="m-0"
>
<Input
onPressEnter={(e) => e.preventDefault()}
placeholder={t('flow.pasteFileLink')}
suffix={
<Button
type="primary"
onClick={onOk}
size={'small'}
loading={loading}
>
{t('common.submit')}
</Button>
}
/>
</Form.Item>
<Form.Item name={'result'} noStyle />
</Form>
);
return (
<Popover
content={content}
open={visible}
trigger={'click'}
onOpenChange={switchVisible}
>
{children}
</Popover>
);
};

View File

@ -1,21 +0,0 @@
.title {
flex-basis: 60px;
}
.formWrapper {
:global(.ant-form-item-label) {
font-weight: 600;
}
}
.operatorDescription {
font-size: 14px;
padding-top: 16px;
font-weight: normal;
}
.formDrawer {
:global(.ant-drawer-content-wrapper) {
transform: translateX(0) !important;
}
}

View File

@ -1,218 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { CloseOutlined } from '@ant-design/icons';
import { Drawer, Flex, Form, Input } from 'antd';
import { get, isPlainObject, lowerFirst } from 'lodash';
import { Play } from 'lucide-react';
import { useEffect, useRef } from 'react';
import { BeginId, Operator, operatorMap } from '../constant';
import AkShareForm from '../form/akshare-form';
import AnswerForm from '../form/answer-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 GenerateForm from '../form/generate-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 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';
import RewriteQuestionForm from '../form/rewrite-question-form';
import SwitchForm from '../form/switch-form';
import TemplateForm from '../form/template-form';
import TuShareForm from '../form/tushare-form';
import WenCaiForm from '../form/wencai-form';
import WikipediaForm from '../form/wikipedia-form';
import YahooFinanceForm from '../form/yahoo-finance-form';
import { useHandleFormValuesChange, useHandleNodeNameChange } from '../hooks';
import OperatorIcon from '../operator-icon';
import {
buildCategorizeListFromObject,
getDrawerWidth,
needsSingleStepDebugging,
} from '../utils';
import SingleDebugDrawer from './single-debug-drawer';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { FlowFormContext } from '../context';
import { RunTooltip } from '../flow-tooltip';
import IterationForm from '../form/iteration-from';
import styles from './index.less';
interface IProps {
node?: RAGFlowNodeType;
singleDebugDrawerVisible: IModalProps<any>['visible'];
hideSingleDebugDrawer: IModalProps<any>['hideModal'];
showSingleDebugDrawer: IModalProps<any>['showModal'];
}
const FormMap = {
[Operator.Begin]: BeginForm,
[Operator.Retrieval]: RetrievalForm,
[Operator.Generate]: GenerateForm,
[Operator.Answer]: AnswerForm,
[Operator.Categorize]: CategorizeForm,
[Operator.Message]: MessageForm,
[Operator.Relevant]: RelevantForm,
[Operator.RewriteQuestion]: RewriteQuestionForm,
[Operator.Baidu]: BaiduForm,
[Operator.DuckDuckGo]: DuckDuckGoForm,
[Operator.KeywordExtract]: KeywordExtractForm,
[Operator.Wikipedia]: WikipediaForm,
[Operator.PubMed]: PubMedForm,
[Operator.ArXiv]: ArXivForm,
[Operator.Google]: GoogleForm,
[Operator.Bing]: BingForm,
[Operator.GoogleScholar]: GoogleScholarForm,
[Operator.DeepL]: DeepLForm,
[Operator.GitHub]: GithubForm,
[Operator.BaiduFanyi]: BaiduFanyiForm,
[Operator.QWeather]: QWeatherForm,
[Operator.ExeSQL]: ExeSQLForm,
[Operator.Switch]: SwitchForm,
[Operator.WenCai]: WenCaiForm,
[Operator.AkShare]: AkShareForm,
[Operator.YahooFinance]: YahooFinanceForm,
[Operator.Jin10]: Jin10Form,
[Operator.TuShare]: TuShareForm,
[Operator.Crawler]: CrawlerForm,
[Operator.Invoke]: InvokeForm,
[Operator.Concentrator]: () => <></>,
[Operator.Note]: () => <></>,
[Operator.Template]: TemplateForm,
[Operator.Email]: EmailForm,
[Operator.Iteration]: IterationForm,
[Operator.IterationStart]: () => <></>,
[Operator.Code]: CodeForm,
};
const EmptyContent = () => <div></div>;
const FormDrawer = ({
visible,
hideModal,
node,
singleDebugDrawerVisible,
hideSingleDebugDrawer,
showSingleDebugDrawer,
}: IModalProps<any> & IProps) => {
const operatorName: Operator = node?.data.label as Operator;
const OperatorForm = FormMap[operatorName] ?? EmptyContent;
const [form] = Form.useForm();
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
id: node?.id,
data: node?.data,
});
const previousId = useRef<string | undefined>(node?.id);
const { t } = useTranslate('flow');
const { handleValuesChange } = useHandleFormValuesChange(node?.id);
useEffect(() => {
if (visible) {
if (node?.id !== previousId.current) {
form.resetFields();
}
if (operatorName === Operator.Categorize) {
const items = buildCategorizeListFromObject(
get(node, 'data.form.category_description', {}),
);
const formData = node?.data?.form;
if (isPlainObject(formData)) {
form.setFieldsValue({ ...formData, items });
}
} else {
form.setFieldsValue(node?.data?.form);
}
previousId.current = node?.id;
}
}, [visible, form, node?.data?.form, node?.id, node, operatorName]);
return (
<Drawer
title={
<Flex vertical>
<Flex gap={'middle'} align="center">
<OperatorIcon
name={operatorName}
color={operatorMap[operatorName]?.color}
></OperatorIcon>
<Flex align="center" gap={'small'} flex={1}>
<label htmlFor="" className={styles.title}>
{t('title')}
</label>
{node?.id === BeginId ? (
<span>{t(BeginId)}</span>
) : (
<Input
value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
)}
</Flex>
{needsSingleStepDebugging(operatorName) && (
<RunTooltip>
<Play
className="size-5 cursor-pointer"
onClick={showSingleDebugDrawer}
/>
</RunTooltip>
)}
<CloseOutlined onClick={hideModal} />
</Flex>
<span className={styles.operatorDescription}>
{t(`${lowerFirst(operatorName)}Description`)}
</span>
</Flex>
}
placement="right"
onClose={hideModal}
open={visible}
getContainer={false}
mask={false}
width={getDrawerWidth()}
closeIcon={null}
rootClassName={styles.formDrawer}
>
<section className={styles.formWrapper}>
{visible && (
<FlowFormContext.Provider value={node}>
<OperatorForm
onValuesChange={handleValuesChange}
form={form}
node={node}
></OperatorForm>
</FlowFormContext.Provider>
)}
</section>
{singleDebugDrawerVisible && (
<SingleDebugDrawer
visible={singleDebugDrawerVisible}
hideModal={hideSingleDebugDrawer}
componentId={node?.id}
></SingleDebugDrawer>
)}
</Drawer>
);
};
export default FormDrawer;

View File

@ -1,81 +0,0 @@
import CopyToClipboard from '@/components/copy-to-clipboard';
import { useDebugSingle, useFetchInputElements } from '@/hooks/flow-hooks';
import { IModalProps } from '@/interfaces/common';
import { CloseOutlined } from '@ant-design/icons';
import { Drawer } from 'antd';
import { isEmpty } from 'lodash';
import { useCallback } 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';
interface IProps {
componentId?: string;
}
const SingleDebugDrawer = ({
componentId,
visible,
hideModal,
}: IModalProps<any> & IProps) => {
const { t } = useTranslation();
const { data: list } = useFetchInputElements(componentId);
const { debugSingle, data, loading } = useDebugSingle();
const onOk = useCallback(
(nextValues: any[]) => {
if (componentId) {
debugSingle({ component_id: componentId, params: nextValues });
}
},
[componentId, debugSingle],
);
const content = JSON.stringify(data, null, 2);
return (
<Drawer
title={
<div className="flex justify-between">
{t('flow.testRun')}
<CloseOutlined onClick={hideModal} />
</div>
}
width={'100%'}
onClose={hideModal}
open={visible}
getContainer={false}
mask={false}
placement={'bottom'}
height={'95%'}
closeIcon={null}
>
<section className="overflow-y-auto">
<DebugContent
parameters={list}
ok={onOk}
isNext={false}
loading={loading}
submitButtonDisabled={list.length === 0}
></DebugContent>
{!isEmpty(data) ? (
<div className="mt-4 rounded-md bg-slate-200 border border-neutral-200">
<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 bg-slate-100"
/>
</div>
) : null}
</section>
</Drawer>
);
};
export default SingleDebugDrawer;

View File

@ -1,3 +0,0 @@
.id {
.linkText();
}

View File

@ -1,36 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { Modal, Typography } from 'antd';
import { useParams } from 'umi';
import styles from './index.less';
const { Paragraph, Link } = Typography;
const FlowIdModal = ({ hideModal }: IModalProps<any>) => {
const { t } = useTranslate('flow');
const { id } = useParams();
return (
<Modal
title={'Agent ID'}
open
onCancel={hideModal}
cancelButtonProps={{ style: { display: 'none' } }}
onOk={hideModal}
okText={t('close', { keyPrefix: 'common' })}
>
<Paragraph copyable={{ text: id }} className={styles.id}>
{id}
</Paragraph>
<Link
href="https://ragflow.io/docs/dev/http_api_reference#create-session-with-an-agent"
target="_blank"
>
{t('howUseId')}
</Link>
</Modal>
);
};
export default FlowIdModal;

View File

@ -1,121 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchFlow, useSettingFlow } from '@/hooks/flow-hooks';
import { normFile } from '@/utils/file-util';
import { PlusOutlined } from '@ant-design/icons';
import { Form, Input, Modal, Radio, Upload } from 'antd';
import React, { useCallback, useEffect } from 'react';
export function useFlowSettingModal() {
const [visibleSettingModal, setVisibleSettingMModal] = React.useState(false);
return {
visibleSettingModal,
setVisibleSettingMModal,
};
}
type FlowSettingModalProps = {
visible: boolean;
hideModal: () => void;
id: string;
};
export const FlowSettingModal = ({
hideModal,
visible,
id,
}: FlowSettingModalProps) => {
const { data, refetch } = useFetchFlow();
const [form] = Form.useForm();
const { t } = useTranslate('flow.settings');
const { loading, settingFlow } = useSettingFlow();
// Initialize form with data when it becomes available
useEffect(() => {
if (data) {
form.setFieldsValue({
title: data.title,
description: data.description,
permission: data.permission,
avatar: data.avatar ? [{ thumbUrl: data.avatar }] : [],
});
}
}, [data, form]);
const handleSubmit = useCallback(async () => {
if (!id) return;
try {
const { avatar, ...others } = await form.validateFields();
const param = {
...others,
id,
avatar: avatar && avatar.length > 0 ? avatar[0].thumbUrl : '',
};
settingFlow(param);
} catch (error) {
console.error('Validation failed:', error);
}
}, [form, id, settingFlow]);
React.useEffect(() => {
if (!loading && refetch && visible) {
refetch();
}
}, [loading, refetch, visible]);
return (
<Modal
confirmLoading={loading}
title={t('agentSetting')}
open={visible}
onCancel={hideModal}
onOk={handleSubmit}
okText={t('save', { keyPrefix: 'common' })}
cancelText={t('cancel', { keyPrefix: 'common' })}
>
<Form
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
layout="horizontal"
style={{ maxWidth: 600 }}
>
<Form.Item
name="title"
label={t('title')}
rules={[{ required: true, message: 'Please input a title!' }]}
>
<Input />
</Form.Item>
<Form.Item
name="avatar"
label={t('photo')}
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
listType="picture-card"
maxCount={1}
beforeUpload={() => false}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
>
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>{t('upload')}</div>
</button>
</Upload>
</Form.Item>
<Form.Item name="description" label={t('description')}>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item
name="permission"
label={t('permissions')}
tooltip={t('permissionsTip')}
rules={[{ required: true }]}
>
<Radio.Group>
<Radio value="me">{t('me')}</Radio>
<Radio value="team">{t('team')}</Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
);
};

View File

@ -1,16 +0,0 @@
.operatorCard {
:global(.ant-card-body) {
padding: 10px;
}
.cubeIcon {
&:hover {
cursor: pointer;
}
}
}
.siderContent {
padding: 10px 4px;
overflow: auto;
height: calc(100vh - 80px);
}

View File

@ -1,75 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Card, Divider, Flex, Layout, Tooltip } from 'antd';
import classNames from 'classnames';
import lowerFirst from 'lodash/lowerFirst';
import React from 'react';
import { Operator, componentMenuList, operatorMap } from '../constant';
import { useHandleDrag } from '../hooks';
import OperatorIcon from '../operator-icon';
import styles from './index.less';
const { Sider } = Layout;
interface IProps {
setCollapsed: (width: boolean) => void;
collapsed: boolean;
}
const dividerProps = {
marginTop: 10,
marginBottom: 10,
padding: 0,
borderBlockColor: '#b4afaf',
borderStyle: 'dotted',
};
const FlowSide = ({ setCollapsed, collapsed }: IProps) => {
const { handleDragStart } = useHandleDrag();
const { t } = useTranslate('flow');
return (
<Sider
collapsible
collapsed={collapsed}
collapsedWidth={0}
theme={'light'}
onCollapse={(value) => setCollapsed(value)}
>
<Flex vertical gap={10} className={styles.siderContent}>
{componentMenuList.map((x) => {
return (
<React.Fragment key={x.name}>
{x.name === Operator.Note && (
<Divider style={dividerProps}></Divider>
)}
{x.name === Operator.DuckDuckGo && (
<Divider style={dividerProps}></Divider>
)}
<Card
key={x.name}
hoverable
draggable
className={classNames(styles.operatorCard)}
onDragStart={handleDragStart(x.name)}
>
<Flex align="center" gap={15}>
<OperatorIcon
name={x.name}
color={operatorMap[x.name].color}
></OperatorIcon>
<section>
<Tooltip title={t(`${lowerFirst(x.name)}Description`)}>
<b>{t(lowerFirst(x.name))}</b>
</Tooltip>
</section>
</Flex>
</Card>
</React.Fragment>
);
})}
</Flex>
</Sider>
);
};
export default FlowSide;

View File

@ -1,19 +0,0 @@
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>
);
};

View File

@ -1,77 +0,0 @@
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;
};
/**
* dumped
* @param nodeId
* @returns
*/
export const useHandleFormSelectChange = (nodeId?: string) => {
const { addEdge, deleteEdgeBySourceAndSourceHandle } = useGraphStore(
(state) => state,
);
const handleSelectChange = useCallback(
(name?: string) => (value?: string) => {
if (nodeId && name) {
if (value) {
addEdge({
source: nodeId,
target: value,
sourceHandle: name,
targetHandle: null,
});
} else {
// clear selected value
deleteEdgeBySourceAndSourceHandle({
source: nodeId,
sourceHandle: name,
});
}
}
},
[addEdge, nodeId, deleteEdgeBySourceAndSourceHandle],
);
return { handleSelectChange };
};
export const useBuildSortOptions = () => {
const { t } = useTranslate('flow');
const options = useMemo(() => {
return ['data', 'relevance'].map((x) => ({
value: x,
label: t(x),
}));
}, [t]);
return options;
};

View File

@ -1,21 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { Form } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const AkShareForm = ({ onValuesChange, form, node }: IOperatorForm) => {
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10} max={99}></TopNItem>
</Form>
);
};
export default AkShareForm;

View File

@ -1,5 +0,0 @@
const AnswerForm = () => {
return <div></div>;
};
export default AnswerForm;

View File

@ -1,36 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Select } from 'antd';
import { useMemo } from 'react';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const ArXivForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const options = useMemo(() => {
return ['submittedDate', 'lastUpdatedDate', 'relevance'].map((x) => ({
value: x,
label: t(x),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item label={t('sortBy')} name={'sort_by'}>
<Select options={options}></Select>
</Form.Item>
</Form>
);
};
export default ArXivForm;

View File

@ -1,71 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { useMemo } from 'react';
import {
BaiduFanyiDomainOptions,
BaiduFanyiSourceLangOptions,
} from '../../constant';
import { IOperatorForm } from '../../interface';
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;

View File

@ -1,21 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { Form } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const BaiduForm = ({ onValuesChange, form, node }: IOperatorForm) => {
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
</Form>
);
};
export default BaiduForm;

View File

@ -1,68 +0,0 @@
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
const BeginDynamicOptions = () => {
return (
<Form.List
name="options"
rules={[
{
validator: async (_, names) => {
if (!names || names.length < 1) {
return Promise.reject(new Error('At least 1 option'));
}
},
},
]}
>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field, index) => (
<Form.Item
label={index === 0 ? 'Options' : ''}
required={false}
key={field.key}
>
<Form.Item
{...field}
validateTrigger={['onChange', 'onBlur']}
rules={[
{
required: true,
whitespace: true,
message: 'Please input option or delete this field.',
},
]}
noStyle
>
<Input
placeholder="option"
style={{ width: '90%', marginRight: 16 }}
/>
</Form.Item>
{fields.length > 1 ? (
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(field.name)}
/>
) : null}
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
icon={<PlusOutlined />}
block
>
Add option
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</>
)}
</Form.List>
);
};
export default BeginDynamicOptions;

View File

@ -1,50 +0,0 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { useCallback, useMemo, useState } from 'react';
import { BeginQuery, IOperatorForm } from '../../interface';
export const useEditQueryRecord = ({ form, onValuesChange }: IOperatorForm) => {
const { setRecord, currentRecord } = useSetSelectedRecord<BeginQuery>();
const { visible, hideModal, showModal } = useSetModalState();
const [index, setIndex] = useState(-1);
const otherThanCurrentQuery = useMemo(() => {
const query: BeginQuery[] = form?.getFieldValue('query') || [];
return query.filter((item, idx) => idx !== index);
}, [form, index]);
const handleEditRecord = useCallback(
(record: BeginQuery) => {
const query: BeginQuery[] = form?.getFieldValue('query') || [];
const nextQuery: BeginQuery[] =
index > -1 ? query.toSpliced(index, 1, record) : [...query, record];
onValuesChange?.(
{ query: nextQuery },
{ query: nextQuery, prologue: form?.getFieldValue('prologue') },
);
hideModal();
},
[form, hideModal, index, onValuesChange],
);
const handleShowModal = useCallback(
(idx?: number, record?: BeginQuery) => {
setIndex(idx ?? -1);
setRecord(record ?? ({} as BeginQuery));
showModal();
},
[setRecord, showModal],
);
return {
ok: handleEditRecord,
currentRecord,
setRecord,
visible,
hideModal,
showModal: handleShowModal,
otherThanCurrentQuery,
};
};

View File

@ -1,24 +0,0 @@
.dynamicInputVariable {
background-color: #ebe9e950;
:global(.ant-collapse-content) {
background-color: #f6f6f657;
}
:global(.ant-collapse-content-box) {
padding: 0 !important;
}
margin-bottom: 20px;
.title {
font-weight: 600;
font-size: 16px;
}
.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}
}
.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}

View File

@ -1,111 +0,0 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { BeginQuery, IOperatorForm } from '../../interface';
import { useEditQueryRecord } from './hooks';
import { ModalForm } from './paramater-modal';
import QueryTable from './query-table';
import styles from './index.less';
type FieldType = {
prologue?: string;
};
const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
const { t } = useTranslation();
const {
ok,
currentRecord,
visible,
hideModal,
showModal,
otherThanCurrentQuery,
} = useEditQueryRecord({
form,
onValuesChange,
});
const handleDeleteRecord = useCallback(
(idx: number) => {
const query = form?.getFieldValue('query') || [];
const nextQuery = query.filter(
(item: BeginQuery, index: number) => index !== idx,
);
onValuesChange?.(
{ query: nextQuery },
{ query: nextQuery, prologue: form?.getFieldValue('prologue') },
);
},
[form, onValuesChange],
);
return (
<Form.Provider
onFormFinish={(name, { values }) => {
if (name === 'queryForm') {
ok(values as BeginQuery);
}
}}
>
<Form
name="basicForm"
onValuesChange={onValuesChange}
autoComplete="off"
form={form}
layout="vertical"
>
<Form.Item<FieldType>
name={'prologue'}
label={t('chat.setAnOpener')}
tooltip={t('chat.setAnOpenerTip')}
initialValue={t('chat.setAnOpenerInitial')}
>
<Input.TextArea autoSize={{ minRows: 5 }} />
</Form.Item>
{/* Create a hidden field to make Form instance record this */}
<Form.Item name="query" noStyle />
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.query !== curValues.query
}
>
{({ getFieldValue }) => {
const query: BeginQuery[] = getFieldValue('query') || [];
return (
<QueryTable
data={query}
showModal={showModal}
deleteRecord={handleDeleteRecord}
></QueryTable>
);
}}
</Form.Item>
<Button
htmlType="button"
style={{ margin: '0 8px' }}
onClick={() => showModal()}
icon={<PlusOutlined />}
block
className={styles.addButton}
>
{t('flow.addItem')}
</Button>
{visible && (
<ModalForm
visible={visible}
hideModal={hideModal}
initialValue={currentRecord}
onOk={ok}
otherThanCurrentQuery={otherThanCurrentQuery}
/>
)}
</Form>
</Form.Provider>
);
};
export default BeginForm;

View File

@ -1,124 +0,0 @@
import { useResetFormOnCloseModal } from '@/hooks/logic-hooks';
import { IModalProps } from '@/interfaces/common';
import { Form, Input, Modal, Select, Switch } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant';
import { BeginQuery } from '../../interface';
import BeginDynamicOptions from './begin-dynamic-options';
export const ModalForm = ({
visible,
initialValue,
hideModal,
otherThanCurrentQuery,
}: IModalProps<BeginQuery> & {
initialValue: BeginQuery;
otherThanCurrentQuery: BeginQuery[];
}) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const options = useMemo(() => {
return Object.values(BeginQueryType).reduce<DefaultOptionType[]>(
(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>
{cur}
</div>
),
value: cur,
},
];
},
[],
);
}, []);
useResetFormOnCloseModal({
form,
visible: visible,
});
useEffect(() => {
form.setFieldsValue(initialValue);
}, [form, initialValue]);
const onOk = () => {
form.submit();
};
return (
<Modal
title={t('flow.variableSettings')}
open={visible}
onOk={onOk}
onCancel={hideModal}
centered
>
<Form form={form} layout="vertical" name="queryForm" autoComplete="false">
<Form.Item
name="type"
label="Type"
rules={[{ required: true }]}
initialValue={BeginQueryType.Line}
>
<Select options={options} />
</Form.Item>
<Form.Item
name="key"
label="Key"
rules={[
{ required: true },
() => ({
validator(_, value) {
if (
!value ||
!otherThanCurrentQuery.some((x) => x.key === value)
) {
return Promise.resolve();
}
return Promise.reject(new Error('The key cannot be repeated!'));
},
}),
]}
>
<Input />
</Form.Item>
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
name="optional"
label={'Optional'}
valuePropName="checked"
initialValue={false}
>
<Switch />
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.type !== curValues.type
}
>
{({ getFieldValue }) => {
const type: BeginQueryType = getFieldValue('type');
return (
type === BeginQueryType.Options && (
<BeginDynamicOptions></BeginDynamicOptions>
)
);
}}
</Form.Item>
</Form>
</Modal>
);
};

View File

@ -1,92 +0,0 @@
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import type { TableProps } from 'antd';
import { Collapse, Space, Table, Tooltip } from 'antd';
import { BeginQuery } from '../../interface';
import { useTranslation } from 'react-i18next';
import styles from './index.less';
interface IProps {
data: BeginQuery[];
deleteRecord(index: number): void;
showModal(index: number, record: BeginQuery): void;
}
const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
const { t } = useTranslation();
const columns: TableProps<BeginQuery>['columns'] = [
{
title: 'Key',
dataIndex: 'key',
key: 'key',
ellipsis: {
showTitle: false,
},
render: (key) => (
<Tooltip placement="topLeft" title={key}>
{key}
</Tooltip>
),
},
{
title: t('flow.name'),
dataIndex: 'name',
key: 'name',
ellipsis: {
showTitle: false,
},
render: (name) => (
<Tooltip placement="topLeft" title={name}>
{name}
</Tooltip>
),
},
{
title: t('flow.type'),
dataIndex: 'type',
key: 'type',
},
{
title: t('flow.optional'),
dataIndex: 'optional',
key: 'optional',
render: (optional) => (optional ? 'Yes' : 'No'),
},
{
title: t('common.action'),
key: 'action',
render: (_, record, idx) => (
<Space>
<EditOutlined onClick={() => showModal(idx, record)} />
<DeleteOutlined
className="cursor-pointer"
onClick={() => deleteRecord(idx)}
/>
</Space>
),
},
];
return (
<Collapse
defaultActiveKey={['1']}
className={styles.dynamicInputVariable}
items={[
{
key: '1',
label: <span className={styles.title}>{t('flow.input')}</span>,
children: (
<Table<BeginQuery>
columns={columns}
dataSource={data}
pagination={false}
/>
),
},
]}
/>
);
};
export default QueryTable;

View File

@ -1,42 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { useMemo } from 'react';
import { BingCountryOptions, BingLanguageOptions } from '../../constant';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const BingForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const options = useMemo(() => {
return ['Webpages', 'News'].map((x) => ({ label: x, value: x }));
}, []);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item label={t('channel')} name={'channel'}>
<Select options={options}></Select>
</Form.Item>
<Form.Item label={t('apiKey')} name={'api_key'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('country')} name={'country'}>
<Select options={BingCountryOptions}></Select>
</Form.Item>
<Form.Item label={t('language')} name={'language'}>
<Select options={BingLanguageOptions}></Select>
</Form.Item>
</Form>
);
};
export default BingForm;

View File

@ -1,225 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
import { useUpdateNodeInternals } from '@xyflow/react';
import {
Button,
Collapse,
Flex,
Form,
FormListFieldData,
Input,
Select,
} from 'antd';
import { FormInstance } from 'antd/lib';
import { humanId } from 'human-id';
import trim from 'lodash/trim';
import {
ChangeEventHandler,
FocusEventHandler,
useCallback,
useEffect,
useState,
} from 'react';
import { Operator } from '../../constant';
import { useBuildFormSelectOptions } from '../../form-hooks';
import styles from './index.less';
interface IProps {
nodeId?: string;
}
interface INameInputProps {
value?: string;
onChange?: (value: string) => void;
otherNames?: string[];
validate(errors: string[]): void;
}
const getOtherFieldValues = (
form: FormInstance,
formListName: string = 'items',
field: FormListFieldData,
latestField: string,
) =>
(form.getFieldValue([formListName]) ?? [])
.map((x: any) => x[latestField])
.filter(
(x: string) =>
x !== form.getFieldValue([formListName, field.name, latestField]),
);
const NameInput = ({
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;
// trigger validation
if (otherNames?.some((x) => x === val)) {
validate([t('nameRepeatedMsg')]);
} else if (trim(val) === '') {
validate([t('nameRequiredMsg')]);
} else {
validate([]);
}
setName(val);
},
[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 FormSet = ({ nodeId, field }: IProps & { field: FormListFieldData }) => {
const form = Form.useFormInstance();
const { t } = useTranslate('flow');
const buildCategorizeToOptions = useBuildFormSelectOptions(
Operator.Categorize,
nodeId,
);
return (
<section>
<Form.Item
label={t('categoryName')}
name={[field.name, 'name']}
validateTrigger={['onChange', 'onBlur']}
rules={[
{
required: true,
whitespace: true,
message: t('nameMessage'),
},
]}
>
<NameInput
otherNames={getOtherFieldValues(form, 'items', field, 'name')}
validate={(errors: string[]) =>
form.setFields([
{
name: ['items', field.name, 'name'],
errors,
},
])
}
></NameInput>
</Form.Item>
<Form.Item label={t('description')} name={[field.name, 'description']}>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label={t('examples')} name={[field.name, 'examples']}>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label={t('nextStep')} name={[field.name, 'to']}>
<Select
allowClear
options={buildCategorizeToOptions(
getOtherFieldValues(form, 'items', field, 'to'),
)}
/>
</Form.Item>
<Form.Item hidden name={[field.name, 'index']}>
<Input />
</Form.Item>
</section>
);
};
const DynamicCategorize = ({ nodeId }: IProps) => {
const updateNodeInternals = useUpdateNodeInternals();
const form = Form.useFormInstance();
const { t } = useTranslate('flow');
return (
<>
<Form.List name="items">
{(fields, { add, remove }) => {
const handleAdd = () => {
const idx = form.getFieldValue([
'items',
fields.at(-1)?.name,
'index',
]);
add({
name: humanId(),
index: fields.length === 0 ? 0 : idx + 1,
});
if (nodeId) updateNodeInternals(nodeId);
};
return (
<Flex gap={18} vertical>
{fields.map((field) => (
<Collapse
size="small"
key={field.key}
className={styles.caseCard}
items={[
{
key: field.key,
label: (
<div className="flex justify-between">
<span>
{form.getFieldValue(['items', field.name, 'name'])}
</span>
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
</div>
),
children: (
<FormSet nodeId={nodeId} field={field}></FormSet>
),
},
]}
></Collapse>
))}
<Button
type="dashed"
onClick={handleAdd}
block
className={styles.addButton}
icon={<PlusOutlined />}
>
{t('addCategory')}
</Button>
</Flex>
);
}}
</Form.List>
</>
);
};
export default DynamicCategorize;

View File

@ -1,45 +0,0 @@
import {
ICategorizeItem,
ICategorizeItemResult,
} from '@/interfaces/database/flow';
import omit from 'lodash/omit';
import { useCallback } from 'react';
import { IOperatorForm } from '../../interface';
/**
* Convert the list in the following form into an object
* {
"items": [
{
"name": "Categorize 1",
"description": "111",
"examples": "ddd",
"to": "Retrieval:LazyEelsStick"
}
]
}
*/
const buildCategorizeObjectFromList = (list: Array<ICategorizeItem>) => {
return list.reduce<ICategorizeItemResult>((pre, cur) => {
if (cur?.name) {
pre[cur.name] = omit(cur, 'name');
}
return pre;
}, {});
};
export const useHandleFormValuesChange = ({
onValuesChange,
}: IOperatorForm) => {
const handleValuesChange = useCallback(
(changedValues: any, values: any) => {
onValuesChange?.(changedValues, {
...omit(values, 'items'),
category_description: buildCategorizeObjectFromList(values.items),
});
},
[onValuesChange],
);
return { handleValuesChange };
};

View File

@ -1,13 +0,0 @@
@lightBackgroundColor: rgba(150, 150, 150, 0.07);
@darkBackgroundColor: rgba(150, 150, 150, 0.12);
.caseCard {
:global(.ant-collapse-content) {
background-color: @darkBackgroundColor;
}
}
.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}

View File

@ -1,43 +0,0 @@
import LLMSelect from '@/components/llm-select';
import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
import DynamicCategorize from './dynamic-categorize';
import { useHandleFormValuesChange } from './hooks';
const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const { handleValuesChange } = useHandleFormValuesChange({
form,
nodeId: node?.id,
onValuesChange,
});
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={handleValuesChange}
initialValues={{ items: [{}] }}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
>
<LLMSelect></LLMSelect>
</Form.Item>
<MessageHistoryWindowSizeItem
initialValue={1}
></MessageHistoryWindowSizeItem>
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>
</Form>
);
};
export default CategorizeForm;

View File

@ -1,66 +0,0 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Select } from 'antd';
import { useTranslation } from 'react-i18next';
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
import { FormCollapse } from '../components/dynamic-input-variable';
type DynamicInputVariableProps = {
name?: string;
node?: RAGFlowNodeType;
};
export const DynamicInputVariable = ({
name = 'arguments',
node,
}: DynamicInputVariableProps) => {
const { t } = useTranslation();
const valueOptions = useBuildComponentIdSelectOptions(
node?.id,
node?.parentId,
);
return (
<FormCollapse title={t('flow.inputVariables')}>
<Form.List name={name}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<div key={key} className="flex items-center gap-2 pb-4">
<Form.Item
{...restField}
name={[name, 'name']}
className="m-0 flex-1"
>
<Input />
</Form.Item>
<Form.Item
{...restField}
name={[name, 'component_id']}
className="m-0 flex-1"
>
<Select
placeholder={t('common.pleaseSelect')}
options={valueOptions}
></Select>
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</div>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
block
icon={<PlusOutlined />}
>
{t('flow.addVariable')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</FormCollapse>
);
};

View File

@ -1,66 +0,0 @@
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Select } from 'antd';
import { useTranslation } from 'react-i18next';
import { FormCollapse } from '../components/dynamic-input-variable';
type DynamicOutputVariableProps = {
name?: string;
};
const options = [
'String',
'Number',
'Boolean',
'Array[String]',
'Array[Number]',
'Object',
].map((x) => ({ label: x, value: x }));
export const DynamicOutputVariable = ({
name = 'output',
}: DynamicOutputVariableProps) => {
const { t } = useTranslation();
return (
<FormCollapse title={t('flow.output')}>
<Form.List name={name}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<div key={key} className="flex items-center gap-2 pb-4">
<Form.Item
{...restField}
name={[name, 'first']}
className="m-0 flex-1"
>
<Input />
</Form.Item>
<Form.Item
{...restField}
name={[name, 'last']}
className="m-0 flex-1"
>
<Select
placeholder={t('common.pleaseSelect')}
options={options}
></Select>
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</div>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
block
icon={<PlusOutlined />}
>
{t('flow.addVariable')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</FormCollapse>
);
};

View File

@ -1,16 +0,0 @@
.languageItem {
margin: 0;
:global(.ant-select-selector) {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
:global(.ant-select-selector:hover) {
border: none !important;
box-shadow: none !important;
}
:global(.ant-select-focused .ant-select-selector) {
border: none !important;
box-shadow: none !important;
}
}

View File

@ -1,73 +0,0 @@
import Editor, { loader } from '@monaco-editor/react';
import { Form, Select } from 'antd';
import { IOperatorForm } from '../../interface';
import { DynamicInputVariable } from './dynamic-input-variable';
import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent';
import { ICodeForm } from '@/interfaces/database/flow';
import { useCallback } from 'react';
import useGraphStore from '../../store';
import styles from './index.less';
loader.config({ paths: { vs: '/vs' } });
const options = [
ProgrammingLanguage.Python,
ProgrammingLanguage.Javascript,
].map((x) => ({ value: x, label: x }));
const CodeForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const formData = node?.data.form as ICodeForm;
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
const handleChange = useCallback(
(value: ProgrammingLanguage) => {
if (node?.id) {
updateNodeForm(
node?.id,
CodeTemplateStrMap[value as ProgrammingLanguage],
['script'],
);
}
},
[node?.id, updateNodeForm],
);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={'script'}
label={
<Form.Item name={'lang'} className={styles.languageItem}>
<Select
defaultValue={ProgrammingLanguage.Python}
popupMatchSelectWidth={false}
options={options}
onChange={handleChange}
/>
</Form.Item>
}
className="bg-gray-100 rounded dark:bg-gray-800"
>
<Editor
height={600}
theme="vs-dark"
language={formData.lang}
options={{
minimap: { enabled: false },
automaticLayout: true,
}}
/>
</Form.Item>
</Form>
);
};
export default CodeForm;

View File

@ -1,133 +0,0 @@
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 { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
import styles from './index.less';
interface IProps {
name?: string;
node?: RAGFlowNodeType;
title?: string;
}
enum VariableType {
Reference = 'reference',
Input = 'input',
}
const getVariableName = (type: string) =>
type === VariableType.Reference ? 'component_id' : 'value';
const DynamicVariableForm = ({ name: formName, node }: IProps) => {
const nextFormName = formName || 'query';
const { t } = useTranslation();
const valueOptions = useBuildComponentIdSelectOptions(
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([nextFormName, name, 'component_id'], undefined);
form.setFieldValue([nextFormName, name, 'value'], undefined);
}, 0);
},
[form, nextFormName],
);
return (
<Form.List name={nextFormName}>
{(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([nextFormName, 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 = ({ name, node, title }: IProps) => {
const { t } = useTranslation();
return (
<FormCollapse title={title || t('flow.input')}>
<DynamicVariableForm name={name} node={node}></DynamicVariableForm>
</FormCollapse>
);
};
export default DynamicInputVariable;

View File

@ -1,22 +0,0 @@
.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;
}
}

View File

@ -1,17 +0,0 @@
import { Form } from 'antd';
import { IOperatorForm } from '../../interface';
const ConcentratorForm = ({ onValuesChange, form }: IOperatorForm) => {
return (
<Form
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
></Form>
);
};
export default ConcentratorForm;

View File

@ -1,38 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { useMemo } from 'react';
import { CrawlerResultOptions } from '../../constant';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const CrawlerForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const crawlerResultOptions = useMemo(() => {
return CrawlerResultOptions.map((x) => ({
value: x,
label: t(`crawlerResultOptions.${x}`),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('proxy')} name={'proxy'}>
<Input placeholder="like: http://127.0.0.1:8888"></Input>
</Form.Item>
<Form.Item
label={t('extractType')}
name={'extract_type'}
initialValue="markdown"
>
<Select options={crawlerResultOptions}></Select>
</Form.Item>
</Form>
);
};
export default CrawlerForm;

View File

@ -1,36 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Select } from 'antd';
import { DeepLSourceLangOptions, DeepLTargetLangOptions } from '../../constant';
import { useBuildSortOptions } from '../../form-hooks';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const DeepLForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const options = useBuildSortOptions();
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={5}></TopNItem>
<Form.Item label={t('authKey')} name={'auth_key'}>
<Select options={options}></Select>
</Form.Item>
<Form.Item label={t('sourceLang')} name={'source_lang'}>
<Select options={DeepLSourceLangOptions}></Select>
</Form.Item>
<Form.Item label={t('targetLang')} name={'target_lang'}>
<Select options={DeepLTargetLangOptions}></Select>
</Form.Item>
</Form>
);
};
export default DeepLForm;

View File

@ -1,38 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Select } from 'antd';
import { useMemo } from 'react';
import { Channel } from '../../constant';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const DuckDuckGoForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const options = useMemo(() => {
return Object.values(Channel).map((x) => ({ value: x, label: t(x) }));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item
label={t('channel')}
name={'channel'}
tooltip={t('channelTip')}
initialValue={'text'}
>
<Select options={options}></Select>
</Form.Item>
</Form>
);
};
export default DuckDuckGoForm;

View File

@ -1,53 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const EmailForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
{/* SMTP服务器配置 */}
<Form.Item label={t('smtpServer')} name={'smtp_server'}>
<Input placeholder="smtp.example.com" />
</Form.Item>
<Form.Item label={t('smtpPort')} name={'smtp_port'}>
<Input type="number" placeholder="587" />
</Form.Item>
<Form.Item label={t('senderEmail')} name={'email'}>
<Input placeholder="sender@example.com" />
</Form.Item>
<Form.Item label={t('authCode')} name={'password'}>
<Input.Password placeholder="your_password" />
</Form.Item>
<Form.Item label={t('senderName')} name={'sender_name'}>
<Input placeholder="Sender Name" />
</Form.Item>
{/* 动态参数说明 */}
<div style={{ marginBottom: 24 }}>
<h4>{t('dynamicParameters')}</h4>
<div>{t('jsonFormatTip')}</div>
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4 }}>
{`{
"to_email": "recipient@example.com",
"cc_email": "cc@example.com",
"subject": "Email Subject",
"content": "Email Content"
}`}
</pre>
</div>
</Form>
);
};
export default EmailForm;

View File

@ -1,88 +0,0 @@
import LLMSelect from '@/components/llm-select';
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { useTestDbConnect } from '@/hooks/flow-hooks';
import { Button, Flex, Form, Input, InputNumber, Select } from 'antd';
import { useCallback } from 'react';
import { ExeSQLOptions } from '../../constant';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const ExeSQLForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const { testDbConnect, loading } = useTestDbConnect();
const handleTest = useCallback(async () => {
const ret = await form?.validateFields();
testDbConnect(ret);
}, [form, testDbConnect]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
>
<LLMSelect></LLMSelect>
</Form.Item>
<Form.Item
label={t('dbType')}
name={'db_type'}
rules={[{ required: true }]}
>
<Select options={ExeSQLOptions}></Select>
</Form.Item>
<Form.Item
label={t('database')}
name={'database'}
rules={[{ required: true }]}
>
<Input></Input>
</Form.Item>
<Form.Item
label={t('username')}
name={'username'}
rules={[{ required: true }]}
>
<Input></Input>
</Form.Item>
<Form.Item label={t('host')} name={'host'} rules={[{ required: true }]}>
<Input></Input>
</Form.Item>
<Form.Item label={t('port')} name={'port'} rules={[{ required: true }]}>
<InputNumber></InputNumber>
</Form.Item>
<Form.Item
label={t('password')}
name={'password'}
rules={[{ required: true }]}
>
<Input.Password></Input.Password>
</Form.Item>
<Form.Item
label={t('loop')}
name={'loop'}
tooltip={t('loopTip')}
rules={[{ required: true }]}
>
<InputNumber></InputNumber>
</Form.Item>
<TopNItem initialValue={30} max={1000}></TopNItem>
<Flex justify={'end'}>
<Button type={'primary'} loading={loading} onClick={handleTest}>
Test
</Button>
</Flex>
</Form>
);
};
export default ExeSQLForm;

View File

@ -1,101 +0,0 @@
import { EditableCell, EditableRow } from '@/components/editable-cell';
import { useTranslate } from '@/hooks/common-hooks';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { DeleteOutlined } from '@ant-design/icons';
import { Button, Flex, Select, Table, TableProps } from 'antd';
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
import { IGenerateParameter } from '../../interface';
import { useHandleOperateParameters } from './hooks';
import styles from './index.less';
interface IProps {
node?: RAGFlowNodeType;
}
const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};
const DynamicParameters = ({ node }: IProps) => {
const nodeId = node?.id;
const { t } = useTranslate('flow');
const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId);
const {
dataSource,
handleAdd,
handleRemove,
handleSave,
handleComponentIdChange,
} = useHandleOperateParameters(nodeId!);
const columns: TableProps<IGenerateParameter>['columns'] = [
{
title: t('key'),
dataIndex: 'key',
key: 'key',
width: '40%',
onCell: (record: IGenerateParameter) => ({
record,
editable: true,
dataIndex: 'key',
title: 'key',
handleSave,
}),
},
{
title: t('value'),
dataIndex: 'component_id',
key: 'component_id',
align: 'center',
width: '40%',
render(text, record) {
return (
<Select
style={{ width: '100%' }}
allowClear
options={options}
value={text}
onChange={handleComponentIdChange(record)}
/>
);
},
},
{
title: t('operation'),
dataIndex: 'operation',
width: 20,
key: 'operation',
align: 'center',
fixed: 'right',
render(_, record) {
return <DeleteOutlined onClick={handleRemove(record.id)} />;
},
},
];
return (
<section>
<Flex justify="end">
<Button size="small" onClick={handleAdd}>
{t('add')}
</Button>
</Flex>
<Table
dataSource={dataSource}
columns={columns}
rowKey={'id'}
className={styles.variableTable}
components={components}
rowClassName={() => styles.editableRow}
scroll={{ x: true }}
bordered
/>
</section>
);
};
export default DynamicParameters;

View File

@ -1,70 +0,0 @@
import get from 'lodash/get';
import { useCallback, useMemo } from 'react';
import { v4 as uuid } from 'uuid';
import { IGenerateParameter } from '../../interface';
import useGraphStore from '../../store';
export const useHandleOperateParameters = (nodeId: string) => {
const { getNode, updateNodeForm } = useGraphStore((state) => state);
const node = getNode(nodeId);
const dataSource: IGenerateParameter[] = useMemo(
() => get(node, 'data.form.parameters', []) as IGenerateParameter[],
[node],
);
const handleComponentIdChange = useCallback(
(row: IGenerateParameter) => (value: string) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.id === item.id);
const item = newData[index];
newData.splice(index, 1, {
...item,
component_id: value,
});
updateNodeForm(nodeId, { parameters: newData });
},
[updateNodeForm, nodeId, dataSource],
);
const handleRemove = useCallback(
(id?: string) => () => {
const newData = dataSource.filter((item) => item.id !== id);
updateNodeForm(nodeId, { parameters: newData });
},
[updateNodeForm, nodeId, dataSource],
);
const handleAdd = useCallback(() => {
updateNodeForm(nodeId, {
parameters: [
...dataSource,
{
id: uuid(),
key: '',
component_id: undefined,
},
],
});
}, [dataSource, nodeId, updateNodeForm]);
const handleSave = (row: IGenerateParameter) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.id === item.id);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row,
});
updateNodeForm(nodeId, { parameters: newData });
};
return {
handleAdd,
handleRemove,
handleComponentIdChange,
handleSave,
dataSource,
};
};

View File

@ -1,21 +0,0 @@
.variableTable {
margin-top: 14px;
}
.editableRow {
:global(.editable-cell) {
position: relative;
}
:global(.editable-cell-value-wrap) {
padding: 5px 12px;
cursor: pointer;
height: 30px !important;
}
&:hover {
:global(.editable-cell-value-wrap) {
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 2px;
}
}
}

View File

@ -1,72 +0,0 @@
import LLMSelect from '@/components/llm-select';
import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item';
import { PromptEditor } from '@/components/prompt-editor';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Switch } from 'antd';
import { IOperatorForm } from '../../interface';
import LLMToolsSelect from '@/components/llm-tools-select';
import { useState } from 'react';
const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => {
const { t } = useTranslate('flow');
const [isCurrentLlmSupportTools, setCurrentLlmSupportTools] = useState(false);
const onLlmSelectChanged = (_: string, option: any) => {
setCurrentLlmSupportTools(option.is_tools);
};
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
>
<LLMSelect onInitialValue={onLlmSelectChanged} onChange={onLlmSelectChanged}></LLMSelect>
</Form.Item>
<Form.Item
name={['prompt']}
label={t('systemPrompt')}
initialValue={t('promptText')}
tooltip={t('promptTip')}
rules={[
{
required: true,
message: t('promptMessage'),
},
]}
>
{/* <Input.TextArea rows={8}></Input.TextArea> */}
<PromptEditor></PromptEditor>
</Form.Item>
<Form.Item
name={'llm_enabled_tools'}
label={t('modelEnabledTools', { keyPrefix: 'chat' })}
tooltip={t('modelEnabledToolsTip', { keyPrefix: 'chat' })}
>
<LLMToolsSelect disabled={!isCurrentLlmSupportTools}></LLMToolsSelect>
</Form.Item>
<Form.Item
name={['cite']}
label={t('cite')}
initialValue={true}
valuePropName="checked"
tooltip={t('citeTip')}
>
<Switch />
</Form.Item>
<MessageHistoryWindowSizeItem
initialValue={12}
></MessageHistoryWindowSizeItem>
</Form>
);
};
export default GenerateForm;

View File

@ -1,21 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { Form } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const GithubForm = ({ onValuesChange, form, node }: IOperatorForm) => {
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={5}></TopNItem>
</Form>
);
};
export default GithubForm;

View File

@ -1,34 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { GoogleCountryOptions, GoogleLanguageOptions } from '../../constant';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const GoogleForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item label={t('apiKey')} name={'api_key'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('country')} name={'country'}>
<Select options={GoogleCountryOptions}></Select>
</Form.Item>
<Form.Item label={t('language')} name={'language'}>
<Select options={GoogleLanguageOptions}></Select>
</Form.Item>
</Form>
);
};
export default GoogleForm;

View File

@ -1,75 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { DatePicker, DatePickerProps, Form, Select, Switch } from 'antd';
import dayjs from 'dayjs';
import { useCallback, useMemo } from 'react';
import { useBuildSortOptions } from '../../form-hooks';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const YearPicker = ({
onChange,
value,
}: {
onChange?: (val: number | undefined) => void;
value?: number | undefined;
}) => {
const handleChange: DatePickerProps['onChange'] = useCallback(
(val: any) => {
const nextVal = val?.format('YYYY');
onChange?.(nextVal ? Number(nextVal) : undefined);
},
[onChange],
);
// The year needs to be converted into a number and saved to the backend
const nextValue = useMemo(() => {
if (value) {
return dayjs(value.toString());
}
return undefined;
}, [value]);
return <DatePicker picker="year" onChange={handleChange} value={nextValue} />;
};
const GoogleScholarForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const options = useBuildSortOptions();
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={5}></TopNItem>
<Form.Item
label={t('sortBy')}
name={'sort_by'}
initialValue={'relevance'}
>
<Select options={options}></Select>
</Form.Item>
<Form.Item label={t('yearLow')} name={'year_low'}>
<YearPicker />
</Form.Item>
<Form.Item label={t('yearHigh')} name={'year_high'}>
<YearPicker />
</Form.Item>
<Form.Item
label={t('patents')}
name={'patents'}
valuePropName="checked"
initialValue={true}
>
<Switch></Switch>
</Form.Item>
</Form>
);
};
export default GoogleScholarForm;

View File

@ -1,130 +0,0 @@
import { EditableCell, EditableRow } from '@/components/editable-cell';
import { useTranslate } from '@/hooks/common-hooks';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { DeleteOutlined } from '@ant-design/icons';
import { Button, Collapse, Flex, Input, Select, Table, TableProps } from 'antd';
import { trim } from 'lodash';
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
import { IInvokeVariable } from '../../interface';
import { useHandleOperateParameters } from './hooks';
import styles from './index.less';
interface IProps {
node?: RAGFlowNodeType;
}
const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};
const DynamicVariablesForm = ({ node }: IProps) => {
const nodeId = node?.id;
const { t } = useTranslate('flow');
const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId);
const {
dataSource,
handleAdd,
handleRemove,
handleSave,
handleComponentIdChange,
handleValueChange,
} = useHandleOperateParameters(nodeId!);
const columns: TableProps<IInvokeVariable>['columns'] = [
{
title: t('key'),
dataIndex: 'key',
key: 'key',
onCell: (record: IInvokeVariable) => ({
record,
editable: true,
dataIndex: 'key',
title: 'key',
handleSave,
}),
},
{
title: t('componentId'),
dataIndex: 'component_id',
key: 'component_id',
align: 'center',
width: 140,
render(text, record) {
return (
<Select
style={{ width: '100%' }}
allowClear
options={options}
value={text}
disabled={trim(record.value) !== ''}
onChange={handleComponentIdChange(record)}
/>
);
},
},
{
title: t('value'),
dataIndex: 'value',
key: 'value',
align: 'center',
width: 140,
render(text, record) {
return (
<Input
value={text}
disabled={!!record.component_id}
onChange={handleValueChange(record)}
/>
);
},
},
{
title: t('operation'),
dataIndex: 'operation',
width: 20,
key: 'operation',
align: 'center',
fixed: 'right',
render(_, record) {
return <DeleteOutlined onClick={handleRemove(record.id)} />;
},
},
];
return (
<Collapse
className={styles.dynamicParameterVariable}
defaultActiveKey={['1']}
items={[
{
key: '1',
label: (
<Flex justify={'space-between'}>
<span className={styles.title}>{t('parameter')}</span>
<Button size="small" onClick={handleAdd}>
{t('add')}
</Button>
</Flex>
),
children: (
<Table
dataSource={dataSource}
columns={columns}
rowKey={'id'}
components={components}
rowClassName={() => styles.editableRow}
scroll={{ x: true }}
bordered
/>
),
},
]}
/>
);
};
export default DynamicVariablesForm;

View File

@ -1,97 +0,0 @@
import get from 'lodash/get';
import {
ChangeEventHandler,
MouseEventHandler,
useCallback,
useMemo,
} from 'react';
import { v4 as uuid } from 'uuid';
import { IGenerateParameter, IInvokeVariable } from '../../interface';
import useGraphStore from '../../store';
export const useHandleOperateParameters = (nodeId: string) => {
const { getNode, updateNodeForm } = useGraphStore((state) => state);
const node = getNode(nodeId);
const dataSource: IGenerateParameter[] = useMemo(
() => get(node, 'data.form.variables', []) as IGenerateParameter[],
[node],
);
const changeValue = useCallback(
(row: IInvokeVariable, field: string, value: string) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.id === item.id);
const item = newData[index];
newData.splice(index, 1, {
...item,
[field]: value,
});
updateNodeForm(nodeId, { variables: newData });
},
[dataSource, nodeId, updateNodeForm],
);
const handleComponentIdChange = useCallback(
(row: IInvokeVariable) => (value: string) => {
changeValue(row, 'component_id', value);
},
[changeValue],
);
const handleValueChange = useCallback(
(row: IInvokeVariable): ChangeEventHandler<HTMLInputElement> =>
(e) => {
changeValue(row, 'value', e.target.value);
},
[changeValue],
);
const handleRemove = useCallback(
(id?: string) => () => {
const newData = dataSource.filter((item) => item.id !== id);
updateNodeForm(nodeId, { variables: newData });
},
[updateNodeForm, nodeId, dataSource],
);
const handleAdd: MouseEventHandler = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
updateNodeForm(nodeId, {
variables: [
...dataSource,
{
id: uuid(),
key: '',
component_id: undefined,
value: '',
},
],
});
},
[dataSource, nodeId, updateNodeForm],
);
const handleSave = (row: IGenerateParameter) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.id === item.id);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row,
});
updateNodeForm(nodeId, { variables: newData });
};
return {
handleAdd,
handleRemove,
handleComponentIdChange,
handleValueChange,
handleSave,
dataSource,
};
};

View File

@ -1,44 +0,0 @@
.editableRow {
:global(.editable-cell) {
position: relative;
}
:global(.editable-cell-value-wrap) {
padding: 5px 12px;
cursor: pointer;
height: 30px !important;
}
&:hover {
:global(.editable-cell-value-wrap) {
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 2px;
}
}
}
.dynamicParameterVariable {
background-color: #ebe9e950;
:global(.ant-collapse-content) {
background-color: #f6f6f634;
}
:global(.ant-collapse-content-box) {
padding: 0 !important;
}
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;
}
}

View File

@ -1,87 +0,0 @@
import Editor, { loader } from '@monaco-editor/react';
import { Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { useTranslation } from 'react-i18next';
import { IOperatorForm } from '../../interface';
import DynamicVariablesForm from './dynamic-variables';
loader.config({ paths: { vs: '/vs' } });
enum Method {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
}
const MethodOptions = [Method.GET, Method.POST, Method.PUT].map((x) => ({
label: x,
value: x,
}));
interface TimeoutInputProps {
value?: number;
onChange?: (value: number | null) => void;
}
const TimeoutInput = ({ value, onChange }: TimeoutInputProps) => {
const { t } = useTranslation();
return (
<Space>
<InputNumber value={value} onChange={onChange} /> {t('common.s')}
</Space>
);
};
const InvokeForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslation();
return (
<>
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<Form.Item name={'url'} label={t('flow.url')}>
<Input />
</Form.Item>
<Form.Item
name={'method'}
label={t('flow.method')}
initialValue={Method.GET}
>
<Select options={MethodOptions} />
</Form.Item>
<Form.Item name={'timeout'} label={t('flow.timeout')}>
<TimeoutInput></TimeoutInput>
</Form.Item>
<Form.Item name={'headers'} label={t('flow.headers')}>
<Editor height={200} defaultLanguage="json" theme="vs-dark" />
</Form.Item>
<Form.Item name={'proxy'} label={t('flow.proxy')}>
<Input />
</Form.Item>
<Form.Item
name={'clean_html'}
label={t('flow.cleanHtml')}
tooltip={t('flow.cleanHtmlTip')}
>
<Switch />
</Form.Item>
<Form.Item name={'datatype'} label={t('flow.datatype')}>
<Select
options={[
{ value: 'json', label: 'application/json' },
{ value: 'formdata', label: 'multipart/form-data' },
]}
allowClear={true}
></Select>
</Form.Item>
<DynamicVariablesForm node={node}></DynamicVariablesForm>
</Form>
</>
);
};
export default InvokeForm;

View File

@ -1,94 +0,0 @@
import { CommaIcon, SemicolonIcon } from '@/assets/icon/next-icon';
import { Form, Select } from 'antd';
import {
CornerDownLeft,
IndentIncrease,
Minus,
Slash,
Underline,
} from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const optionList = [
{
value: ',',
icon: CommaIcon,
text: 'comma',
},
{
value: '\n',
icon: CornerDownLeft,
text: 'lineBreak',
},
{
value: 'tab',
icon: IndentIncrease,
text: 'tab',
},
{
value: '_',
icon: Underline,
text: 'underline',
},
{
value: '/',
icon: Slash,
text: 'diagonal',
},
{
value: '-',
icon: Minus,
text: 'minus',
},
{
value: ';',
icon: SemicolonIcon,
text: 'semicolon',
},
];
const IterationForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslation();
const options = useMemo(() => {
return optionList.map((x) => {
let Icon = x.icon;
return {
value: x.value,
label: (
<div className="flex items-center gap-2">
<Icon className={'size-4'}></Icon>
{t(`flow.delimiterOptions.${x.text}`)}
</div>
),
};
});
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={['delimiter']}
label={t('knowledgeDetails.delimiter')}
initialValue={`\\n!?;。;!?`}
rules={[{ required: true }]}
tooltip={t('flow.delimiterTip')}
>
<Select options={options}></Select>
</Form.Item>
</Form>
);
};
export default IterationForm;

View File

@ -1,145 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { useMemo } from 'react';
import {
Jin10CalendarDatashapeOptions,
Jin10CalendarTypeOptions,
Jin10FlashTypeOptions,
Jin10SymbolsDatatypeOptions,
Jin10SymbolsTypeOptions,
Jin10TypeOptions,
} from '../../constant';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const jin10TypeOptions = useMemo(() => {
return Jin10TypeOptions.map((x) => ({
value: x,
label: t(`jin10TypeOptions.${x}`),
}));
}, [t]);
const jin10FlashTypeOptions = useMemo(() => {
return Jin10FlashTypeOptions.map((x) => ({
value: x,
label: t(`jin10FlashTypeOptions.${x}`),
}));
}, [t]);
const jin10CalendarTypeOptions = useMemo(() => {
return Jin10CalendarTypeOptions.map((x) => ({
value: x,
label: t(`jin10CalendarTypeOptions.${x}`),
}));
}, [t]);
const jin10CalendarDatashapeOptions = useMemo(() => {
return Jin10CalendarDatashapeOptions.map((x) => ({
value: x,
label: t(`jin10CalendarDatashapeOptions.${x}`),
}));
}, [t]);
const jin10SymbolsTypeOptions = useMemo(() => {
return Jin10SymbolsTypeOptions.map((x) => ({
value: x,
label: t(`jin10SymbolsTypeOptions.${x}`),
}));
}, [t]);
const jin10SymbolsDatatypeOptions = useMemo(() => {
return Jin10SymbolsDatatypeOptions.map((x) => ({
value: x,
label: t(`jin10SymbolsDatatypeOptions.${x}`),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('type')} name={'type'} initialValue={'flash'}>
<Select options={jin10TypeOptions}></Select>
</Form.Item>
<Form.Item label={t('secretKey')} name={'secret_key'}>
<Input></Input>
</Form.Item>
<Form.Item noStyle dependencies={['type']}>
{({ getFieldValue }) => {
const type = getFieldValue('type');
switch (type) {
case 'flash':
return (
<>
<Form.Item label={t('flashType')} name={'flash_type'}>
<Select options={jin10FlashTypeOptions}></Select>
</Form.Item>
<Form.Item label={t('contain')} name={'contain'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('filter')} name={'filter'}>
<Input></Input>
</Form.Item>
</>
);
case 'calendar':
return (
<>
<Form.Item label={t('calendarType')} name={'calendar_type'}>
<Select options={jin10CalendarTypeOptions}></Select>
</Form.Item>
<Form.Item
label={t('calendarDatashape')}
name={'calendar_datashape'}
>
<Select options={jin10CalendarDatashapeOptions}></Select>
</Form.Item>
</>
);
case 'symbols':
return (
<>
<Form.Item label={t('symbolsType')} name={'symbols_type'}>
<Select options={jin10SymbolsTypeOptions}></Select>
</Form.Item>
<Form.Item
label={t('symbolsDatatype')}
name={'symbols_datatype'}
>
<Select options={jin10SymbolsDatatypeOptions}></Select>
</Form.Item>
</>
);
case 'news':
return (
<>
<Form.Item label={t('contain')} name={'contain'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('filter')} name={'filter'}>
<Input></Input>
</Form.Item>
</>
);
default:
return <></>;
}
}}
</Form.Item>
</Form>
);
};
export default Jin10Form;

View File

@ -1,32 +0,0 @@
import LLMSelect from '@/components/llm-select';
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const KeywordExtractForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
>
<LLMSelect></LLMSelect>
</Form.Item>
<TopNItem initialValue={3}></TopNItem>
</Form>
);
};
export default KeywordExtractForm;

View File

@ -1,16 +0,0 @@
.dynamicDeleteButton {
position: relative;
top: 4px;
margin: 0 8px;
color: #999;
font-size: 24px;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: #777;
}
&[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
}

View File

@ -1,87 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
import { IOperatorForm } from '../../interface';
import styles from './index.less';
const formItemLayout = {
labelCol: {
sm: { span: 6 },
},
wrapperCol: {
sm: { span: 18 },
},
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
sm: { span: 18, offset: 6 },
},
};
const MessageForm = ({ onValuesChange, form }: IOperatorForm) => {
const { t } = useTranslate('flow');
return (
<Form
name="basic"
{...formItemLayoutWithOutLabel}
onValuesChange={onValuesChange}
autoComplete="off"
form={form}
>
<Form.List name="messages">
{(fields, { add, remove }, {}) => (
<>
{fields.map((field, index) => (
<Form.Item
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
label={index === 0 ? t('msg') : ''}
required={false}
key={field.key}
>
<Form.Item
{...field}
validateTrigger={['onChange', 'onBlur']}
rules={[
{
required: true,
whitespace: true,
message: t('messageMsg'),
},
]}
noStyle
>
<Input.TextArea
rows={4}
placeholder={t('messagePlaceholder')}
style={{ width: '80%' }}
/>
</Form.Item>
{fields.length > 1 ? (
<MinusCircleOutlined
className={styles.dynamicDeleteButton}
onClick={() => remove(field.name)}
/>
) : null}
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
style={{ width: '80%' }}
icon={<PlusOutlined />}
>
{t('addMessage')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
);
};
export default MessageForm;

View File

@ -1,32 +0,0 @@
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const PubMedForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item
label={t('email')}
name={'email'}
tooltip={t('emailTip')}
rules={[{ type: 'email' }]}
>
<Input></Input>
</Form.Item>
</Form>
);
};
export default PubMedForm;

View File

@ -1,88 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { useCallback, useMemo } from 'react';
import {
QWeatherLangOptions,
QWeatherTimePeriodOptions,
QWeatherTypeOptions,
QWeatherUserTypeOptions,
} from '../../constant';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const QWeatherForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const qWeatherLangOptions = useMemo(() => {
return QWeatherLangOptions.map((x) => ({
value: x,
label: t(`qWeatherLangOptions.${x}`),
}));
}, [t]);
const qWeatherTypeOptions = useMemo(() => {
return QWeatherTypeOptions.map((x) => ({
value: x,
label: t(`qWeatherTypeOptions.${x}`),
}));
}, [t]);
const qWeatherUserTypeOptions = useMemo(() => {
return QWeatherUserTypeOptions.map((x) => ({
value: x,
label: t(`qWeatherUserTypeOptions.${x}`),
}));
}, [t]);
const getQWeatherTimePeriodOptions = useCallback(
(userType: string) => {
let options = QWeatherTimePeriodOptions;
if (userType === 'free') {
options = options.slice(0, 3);
}
return options.map((x) => ({
value: x,
label: t(`qWeatherTimePeriodOptions.${x}`),
}));
},
[t],
);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('webApiKey')} name={'web_apikey'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('lang')} name={'lang'}>
<Select options={qWeatherLangOptions}></Select>
</Form.Item>
<Form.Item label={t('type')} name={'type'}>
<Select options={qWeatherTypeOptions}></Select>
</Form.Item>
<Form.Item label={t('userType')} name={'user_type'}>
<Select options={qWeatherUserTypeOptions}></Select>
</Form.Item>
<Form.Item noStyle dependencies={['type', 'user_type']}>
{({ getFieldValue }) =>
getFieldValue('type') === 'weather' && (
<Form.Item label={t('timePeriod')} name={'time_period'}>
<Select
options={getQWeatherTimePeriodOptions(
getFieldValue('user_type'),
)}
></Select>
</Form.Item>
)
}
</Form.Item>
</Form>
);
};
export default QWeatherForm;

View File

@ -1,52 +0,0 @@
import pick from 'lodash/pick';
import { useCallback, useEffect } from 'react';
import { IOperatorForm } from '../../interface';
import useGraphStore from '../../store';
export const useBuildRelevantOptions = () => {
const nodes = useGraphStore((state) => state.nodes);
const buildRelevantOptions = useCallback(
(toList: string[]) => {
return nodes
.filter(
(x) => !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],
);
return buildRelevantOptions;
};
// const getTargetOfEdge = (edges: Edge[], sourceHandle: string) =>
// edges.find((x) => x.sourceHandle === sourceHandle)?.target;
/**
* monitor changes in the connection and synchronize the target to the yes and no fields of the form
* similar to the categorize-form's useHandleFormValuesChange method
* @param param0
*/
export const useWatchConnectionChanges = ({ nodeId, form }: IOperatorForm) => {
// const edges = useGraphStore((state) => state.edges);
const getNode = useGraphStore((state) => state.getNode);
const node = getNode(nodeId);
const watchFormChanges = useCallback(() => {
if (node) {
form?.setFieldsValue(pick(node, ['yes', 'no']));
}
}, [node, form]);
// const watchConnectionChanges = useCallback(() => {
// const edgeList = edges.filter((x) => x.source === nodeId);
// const yes = getTargetOfEdge(edgeList, 'yes');
// const no = getTargetOfEdge(edgeList, 'no');
// form?.setFieldsValue({ yes, no });
// }, [edges, nodeId, form]);
useEffect(() => {
watchFormChanges();
}, [watchFormChanges]);
};

View File

@ -1,49 +0,0 @@
import LLMSelect from '@/components/llm-select';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Select } from 'antd';
import { Operator } from '../../constant';
import { useBuildFormSelectOptions } from '../../form-hooks';
import { IOperatorForm } from '../../interface';
import { useWatchConnectionChanges } from './hooks';
const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const buildRelevantOptions = useBuildFormSelectOptions(
Operator.Relevant,
node?.id,
);
useWatchConnectionChanges({ nodeId: node?.id, form });
return (
<Form
name="basic"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
onValuesChange={onValuesChange}
autoComplete="off"
form={form}
>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
>
<LLMSelect></LLMSelect>
</Form.Item>
<Form.Item label={t('yes')} name={'yes'}>
<Select
allowClear
options={buildRelevantOptions([form?.getFieldValue('no')])}
/>
</Form.Item>
<Form.Item label={t('no')} name={'no'}>
<Select
allowClear
options={buildRelevantOptions([form?.getFieldValue('yes')])}
/>
</Form.Item>
</Form>
);
};
export default RelevantForm;

Some files were not shown because too many files have changed in this diff Show More