Feat: Adjust the style of the canvas node #10703 (#10795)

### What problem does this PR solve?

Feat: Adjust the style of the canvas node #10703


### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-10-27 10:36:36 +08:00
committed by GitHub
parent 50e93d1528
commit 24ab857471
122 changed files with 290 additions and 7156 deletions

8
web/package-lock.json generated
View File

@ -66,7 +66,7 @@
"jsencrypt": "^3.3.2",
"lexical": "^0.23.1",
"lodash": "^4.17.21",
"lucide-react": "^0.542.0",
"lucide-react": "^0.546.0",
"mammoth": "^1.7.2",
"next-themes": "^0.4.6",
"openai-speech-stream-player": "^1.0.8",
@ -25175,9 +25175,9 @@
}
},
"node_modules/lucide-react": {
"version": "0.542.0",
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.542.0.tgz",
"integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==",
"version": "0.546.0",
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.546.0.tgz",
"integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"

View File

@ -79,7 +79,7 @@
"jsencrypt": "^3.3.2",
"lexical": "^0.23.1",
"lodash": "^4.17.21",
"lucide-react": "^0.542.0",
"lucide-react": "^0.546.0",
"mammoth": "^1.7.2",
"next-themes": "^0.4.6",
"openai-speech-stream-player": "^1.0.8",

View File

@ -5,7 +5,12 @@ import {
} from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
import { CollapsibleProps } from '@radix-ui/react-collapsible';
import { ChevronDown, ChevronUp } from 'lucide-react';
import {
ChevronDown,
ChevronUp,
ListChevronsDownUp,
ListChevronsUpDown,
} from 'lucide-react';
import * as React from 'react';
import {
PropsWithChildren,
@ -14,7 +19,6 @@ import {
useEffect,
useState,
} from 'react';
import { IconFontFill } from './icon-font';
type CollapseProps = Omit<CollapsibleProps, 'title'> & {
title?: ReactNode;
@ -54,12 +58,11 @@ export function Collapse({
<CollapsibleTrigger className={'w-full'}>
<section className="flex justify-between items-center">
<div className="flex items-center gap-1">
<IconFontFill
name={`more`}
className={cn('size-4', {
'rotate-90': !currentOpen,
})}
></IconFontFill>
{currentOpen ? (
<ListChevronsUpDown className="size-4" />
) : (
<ListChevronsDownUp className="size-4 text-text-secondary" />
)}
<div
className={cn('text-text-secondary', {
'text-text-primary': open,

View File

@ -13,7 +13,7 @@ const LLMLabel = ({ value }: IProps) => {
const { llmName, fId } = getLlmNameAndFIdByLlmId(value);
return (
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 text-xs text-text-secondary">
<LlmIcon
name={getLLMIconName(fId, llmName)}
width={20}

View File

@ -24,6 +24,7 @@ const buttonVariants = cva(
icon: 'bg-colors-background-inverse-standard text-foreground hover:bg-colors-background-inverse-standard/80',
dashed: 'border border-dashed border-input hover:bg-accent',
transparent: 'bg-transparent hover:bg-accent border',
danger: 'bg-transparent border border-state-error text-state-error',
},
size: {
default: 'h-8 px-2.5 py-1.5 ',

View File

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
'rounded-lg border-border-default border shadow-sm bg-bg-input',
'rounded-lg border-border-default border shadow-sm bg-bg-input hover:shadow-md',
className,
)}
{...props}

View File

@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
'peer h-3.5 w-3.5 shrink-0 rounded-sm border border-text-secondary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className,
)}
{...props}
@ -21,7 +21,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
>
<Check className="h-4 w-4" />
<Check className="h-3.5 w-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));

View File

@ -135,7 +135,9 @@ export function RAGFlowPagination({
return (
<section className="flex items-center justify-end text-text-sub-title-invert">
<span className="mr-4">{t('pagination.total', { total: total })}</span>
<span className="mr-4 text-text-primary">
{t('pagination.total', { total: total })}
</span>
<Pagination className="w-auto mx-0 mr-4">
<PaginationContent>
<PaginationItem>
@ -150,7 +152,7 @@ export function RAGFlowPagination({
) : (
<PaginationItem
key={page}
className={cn({
className={cn('text-text-disabled', {
['bg-bg-card rounded-md text-text-primary']:
currentPage === page,
})}

View File

@ -46,6 +46,18 @@ export const FormTooltip = ({ tooltip }: { tooltip: React.ReactNode }) => {
);
};
export function RAGFlowTooltip({
children,
tooltip,
}: React.PropsWithChildren & { tooltip: React.ReactNode }) {
return (
<Tooltip>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
);
}
export interface AntToolTipProps {
title: React.ReactNode;
children: React.ReactNode;

View File

@ -12,7 +12,6 @@ export const BaseNode = forwardRef<
'relative rounded bg-card text-card-foreground',
className,
selected ? 'border-muted-foreground shadow-lg' : '',
'hover:ring-1',
)}
tabIndex={0}
{...props}

View File

@ -116,5 +116,4 @@ export enum Operator {
Splitter = 'Splitter',
HierarchicalMerger = 'HierarchicalMerger',
Extractor = 'Extractor',
Generate = 'Generate',
}

View File

@ -77,13 +77,6 @@ export const useNavigatePage = () => {
[navigate],
);
const navigateToDataflow = useCallback(
(id: string) => () => {
navigate(`${Routes.DataFlow}/${id}`);
},
[navigate],
);
const navigateToAgentLogs = useCallback(
(id: string) => () => {
navigate(`${Routes.AgentLogPage}/${id}`);
@ -185,7 +178,6 @@ export const useNavigatePage = () => {
navigateToAgentList,
navigateToOldProfile,
navigateToDataflowResult,
navigateToDataflow,
navigateToDataFile,
};
};

View File

@ -3,7 +3,7 @@ export default {
common: {
noResults: 'No results.',
selectPlaceholder: 'select value',
selectAll: 'Select All',
selectAll: 'Select all',
delete: 'Delete',
deleteModalTitle: 'Are you sure to delete this item?',
ok: 'Ok',
@ -1747,6 +1747,9 @@ Important structured information may include: names, dates, locations, events, k
mcpServers: 'MCP Servers',
customizeTheListOfMcpServers: 'Customize the list of MCP servers',
cachedTools: 'cached tools',
bulkManage: 'Bulk manage',
exitBulkManage: 'Exit bulk manage',
selected: 'Selected',
},
search: {
searchApps: 'Search Apps',

View File

@ -1634,6 +1634,9 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
mcpServers: 'MCP 服务器',
customizeTheListOfMcpServers: '自定义 MCP 服务器列表',
cachedTools: '缓存工具',
selected: '已选择',
bulkManage: '批量管理',
exitBulkManage: '退出批量管理',
},
search: {
searchApps: '搜索',

View File

@ -10,6 +10,7 @@ import useGraphStore from '../../store';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { NodeHandleId, Operator } from '../../constant';
@ -91,11 +92,14 @@ function InnerButtonEdge({
);
}, [data?.isHovered, isTargetPlaceholder, sourceHandleId, target]);
const activeMarkerEnd =
selected || !isEmpty(showHighlight) ? 'url(#selected-marker)' : markerEnd;
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
markerEnd={activeMarkerEnd}
style={{
...style,
...selectedStyle,

View File

@ -59,7 +59,6 @@ import { CategorizeNode } from './node/categorize-node';
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { ExtractorNode } from './node/extractor-node';
import { FileNode } from './node/file-node';
import { GenerateNode } from './node/generate-node';
import { InvokeNode } from './node/invoke-node';
import { IterationNode, IterationStartNode } from './node/iteration-node';
import { KeywordNode } from './node/keyword-node';
@ -84,7 +83,6 @@ export const nodeTypes: NodeTypes = {
relevantNode: RelevantNode,
noteNode: NoteNode,
switchNode: SwitchNode,
generateNode: GenerateNode,
retrievalNode: RetrievalNode,
messageNode: MessageNode,
rewriteNode: RewriteNode,
@ -249,6 +247,19 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
style={{ position: 'absolute', top: 10, left: 0 }}
>
<defs>
<marker
fill="rgb(var(--accent-primary))"
id="selected-marker"
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>
<marker
fill="var(--text-disabled)"
id="logo"

View File

@ -1,4 +1,3 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { IAgentNode } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { Handle, NodeProps, Position } from '@xyflow/react';
@ -9,6 +8,7 @@ import { AgentExceptionMethod, NodeHandleId } from '../../constant';
import { AgentFormSchemaType } from '../../form/agent-form';
import useGraphStore from '../../store';
import { hasSubAgent, isBottomSubAgent } from '../../utils';
import { LLMLabelCard } from './card';
import { CommonHandle, LeftEndHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
@ -90,9 +90,7 @@ function InnerAgentNode({
></Handle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<section className="flex flex-col gap-2">
<div className={'bg-bg-card rounded-sm p-1'}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
{(isGotoMethod ||
exceptionMethod === AgentExceptionMethod.Comment) && (
<div className="bg-bg-card rounded-sm p-1 flex justify-between gap-2">

View File

@ -12,6 +12,7 @@ import {
} from '../../constant';
import { BeginQuery } from '../../interface';
import OperatorIcon from '../../operator-icon';
import { LabelCard } from './card';
import { CommonHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import styles from './index.less';
@ -43,15 +44,16 @@ function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
{Object.entries(inputs).map(([key, val], idx) => {
const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType];
return (
<div
key={idx}
className={cn(styles.conditionBlock, 'flex gap-1.5 items-center')}
>
<Icon className="size-4" />
<label htmlFor="">{key}</label>
<span className={styles.parameterValue}>{val.name}</span>
<LabelCard key={idx} className={cn('flex gap-1.5 items-center')}>
<Icon className="size-3.5" />
<label htmlFor="" className="text-accent-primary text-sm italic">
{key}
</label>
<LabelCard className="py-0.5 truncate flex-1">
{val.name}
</LabelCard>
<span className="flex-1">{val.optional ? 'Yes' : 'No'}</span>
</div>
</LabelCard>
);
})}
</section>

View File

@ -1,3 +1,4 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { Button } from '@/components/ui/button';
import {
Card,
@ -16,7 +17,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { PropsWithChildren } from 'react';
@ -65,6 +65,21 @@ type LabelCardProps = {
export function LabelCard({ children, className }: LabelCardProps) {
return (
<div className={cn('bg-bg-card rounded-sm p-1', className)}>{children}</div>
<div
className={cn(
'bg-bg-card rounded-sm p-1 text-text-secondary text-xs',
className,
)}
>
{children}
</div>
);
}
export function LLMLabelCard({ llmId }: { llmId?: string }) {
return (
<LabelCard>
<LLMLabel value={llmId}></LLMLabel>
</LabelCard>
);
}

View File

@ -1,8 +1,8 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { ICategorizeNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react';
import { get } from 'lodash';
import { memo } from 'react';
import { LLMLabelCard } from './card';
import { CommonHandle, LeftEndHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
@ -24,9 +24,7 @@ export function InnerCategorizeNode({
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<section className="flex flex-col gap-2">
<div className={'bg-bg-card rounded-sm px-1'}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
{positions.map((position) => {
return (
<div key={position.uuid}>

View File

@ -96,7 +96,9 @@ export function InnerNextStepDropdown({
onClick={(e) => e.stopPropagation()}
className="w-[300px] font-semibold"
>
<DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel>
<DropdownMenuLabel className="text-xs text-text-primary">
{t('flow.nextStep')}
</DropdownMenuLabel>
<HideModalContext.Provider value={hideModal}>
{isPipeline ? (
<PipelineAccordionOperators></PipelineAccordionOperators>

View File

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

View File

@ -1,60 +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 { memo } from 'react';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function InnerGenerateNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IGenerateNode>) {
const { theme } = useTheme();
return (
<section
className={classNames(
styles.logicNode,
theme === 'dark' ? styles.dark : '',
{
[styles.selectedNode]: selected,
},
)}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
</section>
);
}
export const GenerateNode = memo(InnerGenerateNode);

View File

@ -1,6 +1,6 @@
import { IRagNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react';
import { memo } from 'react';
import { PropsWithChildren, memo } from 'react';
import { NodeHandleId } from '../../constant';
import { needsSingleStepDebugging, showCopyIcon } from '../../utils';
import { CommonHandle, LeftEndHandle } from './handle';
@ -9,12 +9,15 @@ import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar';
type RagNodeProps = NodeProps<IRagNode> & PropsWithChildren;
function InnerRagNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IRagNode>) {
children,
}: RagNodeProps) {
return (
<ToolBar
selected={selected}
@ -35,6 +38,7 @@ function InnerRagNode({
isConnectableEnd={false}
></CommonHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
{children}
</NodeWrapper>
</ToolBar>
);

View File

@ -1,10 +1,10 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { useTheme } from '@/components/theme-provider';
import { IKeywordNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
import { get } from 'lodash';
import { memo } from 'react';
import { LLMLabelCard } from './card';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
@ -50,9 +50,7 @@ export function InnerKeywordNode({
className={styles.nodeHeader}
></NodeHeader>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
</section>
);
}

View File

@ -7,7 +7,7 @@ export function NodeWrapper({ children, className, selected }: IProps) {
return (
<section
className={cn(
'bg-text-title-invert p-2.5 rounded-md w-[200px] text-xs group',
'bg-bg-component p-2.5 rounded-md w-[200px] border border-border-button text-xs group hover:shadow-md',
{ 'border border-accent-primary': selected },
className,
)}

View File

@ -57,7 +57,6 @@ function InnerRetrievalNode({
className="size-6 rounded-lg"
avatar={id}
name={item?.name || (label as string) || 'CN'}
isPerson={true}
/>
<div className={'truncate flex-1'}>{label || item?.name}</div>

View File

@ -1,10 +1,10 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { useTheme } from '@/components/theme-provider';
import { IRewriteNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import classNames from 'classnames';
import { get } from 'lodash';
import { memo } from 'react';
import { LLMLabelCard } from './card';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
@ -50,9 +50,7 @@ function InnerRewriteNode({
className={styles.nodeHeader}
></NodeHeader>
<div className={styles.nodeText}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
<LLMLabelCard llmId={get(data, 'form.llm_id')}></LLMLabelCard>
</section>
);
}

View File

@ -43,7 +43,7 @@ const ConditionBlock = ({
}, []);
return (
<Card>
<Card className="bg-bg-card border-transparent rounded-md">
<CardContent className="p-0 divide-y divide-background-card">
{items.map((x, idx) => (
<div key={idx}>

View File

@ -3,6 +3,7 @@ import {
TooltipNode,
TooltipTrigger,
} from '@/components/xyflow/tooltip-node';
import { cn } from '@/lib/utils';
import { Position } from '@xyflow/react';
import { Copy, Play, Trash2 } from 'lucide-react';
import {
@ -15,9 +16,19 @@ import { Operator } from '../../constant';
import { useDuplicateNode } from '../../hooks';
import useGraphStore from '../../store';
function IconWrapper({ children, ...props }: HTMLAttributes<HTMLDivElement>) {
function IconWrapper({
children,
className,
...props
}: HTMLAttributes<HTMLDivElement>) {
return (
<div className="p-1.5 bg-text-title rounded-sm cursor-pointer" {...props}>
<div
className={cn(
'p-1.5 bg-bg-component border border-border-button rounded-sm cursor-pointer hover:text-text-primary',
className,
)}
{...props}
>
{children}
</div>
);
@ -71,7 +82,7 @@ export function ToolBar({
<TooltipTrigger className="h-full">{children}</TooltipTrigger>
<TooltipContent position={Position.Top}>
<section className="flex gap-2 items-center">
<section className="flex gap-2 items-center text-text-secondary">
{showRun && (
<IconWrapper>
<Play className="size-3.5" data-play />
@ -82,7 +93,10 @@ export function ToolBar({
<Copy className="size-3.5" />
</IconWrapper>
)}
<IconWrapper onClick={deleteNode}>
<IconWrapper
onClick={deleteNode}
className="hover:text-state-error hover:border-state-error"
>
<Trash2 className="size-3.5" />
</IconWrapper>
</section>

View File

@ -15,6 +15,7 @@ import {
useContext,
useMemo,
} from 'react';
import { LabelCard } from '../../canvas/node/card';
import { Operator } from '../../constant';
import { AgentInstanceContext } from '../../context';
import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
@ -34,15 +35,9 @@ export function ToolCard({
}: PropsWithChildren & React.HTMLAttributes<HTMLLIElement>) {
const element = useMemo(() => {
return (
<li
{...props}
className={cn(
'flex bg-bg-card p-1 rounded-sm justify-between',
className,
)}
>
<LabelCard {...props} className={cn('flex justify-between', className)}>
{children}
</li>
</LabelCard>
);
}, [children, className, props]);

View File

@ -8,7 +8,6 @@ import {
} from '@/components/ui/dialog';
import { useSetAgentSetting } from '@/hooks/use-agent-request';
import { IModalProps } from '@/interfaces/common';
import { transformFile2Base64 } from '@/utils/file-util';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -23,11 +22,7 @@ export function SettingDialog({ hideModal }: IModalProps<any>) {
const submit = useCallback(
async (values: SettingFormSchemaType) => {
const avatar = values.avatar;
const code = await setAgentSetting({
...values,
avatar: avatar.length > 0 ? await transformFile2Base64(avatar[0]) : '',
});
const code = await setAgentSetting(values);
if (code === 0) {
hideModal?.();
}
@ -39,7 +34,7 @@ export function SettingDialog({ hideModal }: IModalProps<any>) {
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogTitle>{t('common.edit')}</DialogTitle>
</DialogHeader>
<SettingForm submit={submit}></SettingForm>
<DialogFooter>

View File

@ -1,32 +1,20 @@
import { z } from 'zod';
import {
FileUpload,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDelete,
FileUploadItemMetadata,
FileUploadItemPreview,
FileUploadList,
FileUploadTrigger,
} from '@/components/file-upload';
import { AvatarUpload } from '@/components/avatar-upload';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormItem, FormLabel } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Textarea } from '@/components/ui/textarea';
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { transformBase64ToFile } from '@/utils/file-util';
import { zodResolver } from '@hookform/resolvers/zod';
import { CloudUpload, X } from 'lucide-react';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
const formSchema = z.object({
title: z.string().min(1, {}),
avatar: z.array(z.custom<File>()).optional().nullable(),
avatar: z.string().optional(),
description: z.string().optional().nullable(),
permission: z.string(),
});
@ -55,7 +43,7 @@ export function SettingForm({ submit }: SettingFormProps) {
form.reset({
title: data?.title,
description: data?.description,
avatar: data.avatar ? [transformBase64ToFile(data.avatar)] : [],
avatar: data.avatar,
permission: data?.permission,
});
}, [data, form]);
@ -71,45 +59,7 @@ export function SettingForm({ submit }: SettingFormProps) {
<Input />
</RAGFlowFormItem>
<RAGFlowFormItem name="avatar" label={t('photo')}>
{(field) => (
<FileUpload
value={field.value}
onValueChange={field.onChange}
accept="image/*"
maxFiles={1}
onFileReject={(_, message) => {
form.setError('avatar', {
message,
});
}}
multiple
>
<FileUploadDropzone className="flex-row flex-wrap border-dotted text-center">
<CloudUpload className="size-4" />
Drag and drop or
<FileUploadTrigger asChild>
<Button variant="link" size="sm" className="p-0">
choose files
</Button>
</FileUploadTrigger>
to upload
</FileUploadDropzone>
<FileUploadList>
{field.value?.map((file: File, index: number) => (
<FileUploadItem key={index} value={file}>
<FileUploadItemPreview />
<FileUploadItemMetadata />
<FileUploadItemDelete asChild>
<Button variant="ghost" size="icon" className="size-7">
<X />
<span className="sr-only">Delete</span>
</Button>
</FileUploadItemDelete>
</FileUploadItem>
))}
</FileUploadList>
</FileUpload>
)}
<AvatarUpload></AvatarUpload>
</RAGFlowFormItem>
<RAGFlowFormItem name="description" label={t('description')}>
<Textarea rows={4} />

View File

@ -78,10 +78,7 @@ const buildComponentDownstreamOrUpstream = (
const removeUselessDataInTheOperator = curry(
(operatorName: string, params: Record<string, unknown>) => {
if (
operatorName === Operator.Generate ||
operatorName === Operator.Categorize
) {
if (operatorName === Operator.Categorize) {
return removeUselessFieldsFromValues(params, '');
}
return params;

View File

@ -1,12 +1,9 @@
import { AgentCategory } from '@/constants/agent';
import { AgentCategory, Operator } from '@/constants/agent';
import { useSetModalState } from '@/hooks/common-hooks';
import { EmptyDsl, useSetAgent } from '@/hooks/use-agent-request';
import { DSL } from '@/interfaces/database/agent';
import {
BeginId,
Operator,
initialParserValues,
} from '@/pages/data-flow/constant';
import { FileId, initialParserValues } from '@/pages/agent/constant';
import { useCallback } from 'react';
import { FlowType } from '../constant';
import { FormSchemaType } from '../create-agent-form';
@ -15,15 +12,15 @@ export const DataflowEmptyDsl = {
graph: {
nodes: [
{
id: BeginId,
id: FileId,
type: 'beginNode',
position: {
x: 50,
y: 200,
},
data: {
label: Operator.Begin,
name: Operator.Begin,
label: Operator.File,
name: Operator.File,
},
sourcePosition: 'left',
targetPosition: 'right',
@ -53,7 +50,7 @@ export const DataflowEmptyDsl = {
edges: [
{
id: 'xy-edge__Filestart-Parser:HipSignsRhymeend',
source: BeginId,
source: FileId,
sourceHandle: 'start',
target: 'Parser:HipSignsRhyme',
targetHandle: 'end',
@ -61,9 +58,9 @@ export const DataflowEmptyDsl = {
],
},
components: {
[Operator.Begin]: {
[Operator.File]: {
obj: {
component_name: Operator.Begin,
component_name: Operator.File,
params: {},
},
downstream: [], // other edge target is downstream, edge source is current node id

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,56 +0,0 @@
import {
createContext,
ReactNode,
useCallback,
useContext,
useRef,
} from 'react';
interface DropdownContextType {
canShowDropdown: () => boolean;
setActiveDropdown: (type: 'handle' | 'drag') => void;
clearActiveDropdown: () => void;
}
const DropdownContext = createContext<DropdownContextType | null>(null);
export const useDropdownManager = () => {
const context = useContext(DropdownContext);
if (!context) {
throw new Error('useDropdownManager must be used within DropdownProvider');
}
return context;
};
interface DropdownProviderProps {
children: ReactNode;
}
export const DropdownProvider = ({ children }: DropdownProviderProps) => {
const activeDropdownRef = useRef<'handle' | 'drag' | null>(null);
const canShowDropdown = useCallback(() => {
const current = activeDropdownRef.current;
return !current;
}, []);
const setActiveDropdown = useCallback((type: 'handle' | 'drag') => {
activeDropdownRef.current = type;
}, []);
const clearActiveDropdown = useCallback(() => {
activeDropdownRef.current = null;
}, []);
const value: DropdownContextType = {
canShowDropdown,
setActiveDropdown,
clearActiveDropdown,
};
return (
<DropdownContext.Provider value={value}>
{children}
</DropdownContext.Provider>
);
};

View File

@ -1,110 +0,0 @@
import {
BaseEdge,
Edge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
} from '@xyflow/react';
import { memo } from 'react';
import useGraphStore from '../../store';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils';
import { useMemo } from 'react';
import { Operator } from '../../constant';
function InnerButtonEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
source,
target,
style = {},
markerEnd,
selected,
data,
}: EdgeProps<Edge<{ isHovered: boolean }>>) {
const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const selectedStyle = useMemo(() => {
return selected ? { strokeWidth: 1, stroke: 'rgba(76, 164, 231, 1)' } : {};
}, [selected]);
const onEdgeClick = () => {
deleteEdgeById(id);
};
// highlight the nodes that the workflow passes through
const { data: flowDetail } = useFetchAgent();
const showHighlight = useMemo(() => {
const path = flowDetail?.dsl?.path ?? [];
const idx = path.findIndex((x) => x === target);
if (idx !== -1) {
let index = idx - 1;
while (index >= 0) {
if (path[index] === source) {
return { strokeWidth: 1, stroke: 'var(--accent-primary)' };
}
index--;
}
return {};
}
return {};
}, [flowDetail?.dsl?.path, source, target]);
const visible = useMemo(() => {
return data?.isHovered && source !== Operator.Begin;
}, [data?.isHovered, source]);
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{ ...style, ...selectedStyle, ...showHighlight }}
className="text-text-secondary"
/>
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
fontSize: 12,
// everything inside EdgeLabelRenderer has no pointer events by default
// if you have an interactive element, set pointer-events: all
pointerEvents: 'all',
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
}}
className="nodrag nopan"
>
<button
className={cn(
'size-3.5 border border-state-error text-state-error rounded-full leading-none',
'invisible',
{ visible },
)}
type="button"
onClick={onEdgeClick}
>
×
</button>
</div>
</EdgeLabelRenderer>
</>
);
}
export const ButtonEdge = memo(InnerButtonEdge);

View File

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

View File

@ -1,330 +0,0 @@
import { useIsDarkTheme, useTheme } from '@/components/theme-provider';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useSetModalState } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import {
Connection,
ConnectionMode,
ControlButton,
Controls,
NodeTypes,
OnConnectEnd,
Position,
ReactFlow,
ReactFlowInstance,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { NotebookPen } from 'lucide-react';
import { memo, useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AgentInstanceContext, HandleContext } from '../context';
import FormSheet from '../form-sheet/next';
import { useSelectCanvasData, useValidateConnection } from '../hooks';
import { useAddNode } from '../hooks/use-add-node';
import { useBeforeDelete } from '../hooks/use-before-delete';
import { useMoveNote } from '../hooks/use-move-note';
import { useDropdownManager } from './context';
import { AgentBackground } from '@/components/canvas/background';
import Spotlight from '@/components/spotlight';
import { useRunDataflow } from '../hooks/use-run-dataflow';
import {
useHideFormSheetOnNodeDeletion,
useShowDrawer,
} from '../hooks/use-show-drawer';
import RunSheet from '../run-sheet';
import useGraphStore from '../store';
import { ButtonEdge } from './edge';
import styles from './index.less';
import { RagNode } from './node';
import { BeginNode } from './node/begin-node';
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { ExtractorNode } from './node/extractor-node';
import NoteNode from './node/note-node';
import ParserNode from './node/parser-node';
import { SplitterNode } from './node/splitter-node';
import TokenizerNode from './node/tokenizer-node';
export const nodeTypes: NodeTypes = {
ragNode: RagNode,
beginNode: BeginNode,
noteNode: NoteNode,
parserNode: ParserNode,
tokenizerNode: TokenizerNode,
splitterNode: SplitterNode,
contextNode: ExtractorNode,
};
const edgeTypes = {
buttonEdge: ButtonEdge,
};
interface IProps {
drawerVisible: boolean;
hideDrawer(): void;
showLogSheet(): void;
}
function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
const { t } = useTranslation();
const {
nodes,
edges,
onConnect: originalOnConnect,
onEdgesChange,
onNodesChange,
onSelectionChange,
onEdgeMouseEnter,
onEdgeMouseLeave,
} = useSelectCanvasData();
const isValidConnection = useValidateConnection();
const [reactFlowInstance, setReactFlowInstance] =
useState<ReactFlowInstance<any, any>>();
const {
onNodeClick,
clickedNode,
formDrawerVisible,
hideFormDrawer,
singleDebugDrawerVisible,
hideSingleDebugDrawer,
showSingleDebugDrawer,
chatVisible,
runVisible,
hideRunOrChatDrawer,
showFormDrawer,
} = useShowDrawer({
drawerVisible,
hideDrawer,
});
const { handleBeforeDelete } = useBeforeDelete();
const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance);
const { ref, showImage, hideImage, imgVisible, mouse } = useMoveNote();
const { theme } = useTheme();
const isDarkTheme = useIsDarkTheme();
useHideFormSheetOnNodeDeletion({ hideFormDrawer });
const { visible, hideModal, showModal } = useSetModalState();
const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 });
const isConnectedRef = useRef(false);
const connectionStartRef = useRef<{
nodeId: string;
handleId: string;
} | null>(null);
const preventCloseRef = useRef(false);
const { setActiveDropdown, clearActiveDropdown } = useDropdownManager();
const { hasChildNode } = useGraphStore((state) => state);
const onPaneClick = useCallback(() => {
hideFormDrawer();
if (visible && !preventCloseRef.current) {
hideModal();
clearActiveDropdown();
}
if (imgVisible) {
addNoteNode(mouse);
hideImage();
}
}, [
hideFormDrawer,
visible,
hideModal,
imgVisible,
addNoteNode,
mouse,
hideImage,
clearActiveDropdown,
]);
const { run, loading: running } = useRunDataflow(
showLogSheet!,
hideRunOrChatDrawer,
);
const onConnect = (connection: Connection) => {
originalOnConnect(connection);
isConnectedRef.current = true;
};
const onConnectStart = (event: any, params: any) => {
isConnectedRef.current = false;
if (params && params.nodeId && params.handleId) {
connectionStartRef.current = {
nodeId: params.nodeId,
handleId: params.handleId,
};
} else {
connectionStartRef.current = null;
}
};
const onConnectEnd: OnConnectEnd = (event, connectionState) => {
const target = event.target as HTMLElement;
const nodeId = connectionState.fromNode?.id;
// Events triggered by Handle are directly interrupted
if (
target?.classList.contains('react-flow__handle') ||
(nodeId && hasChildNode(nodeId))
) {
return;
}
if ('clientX' in event && 'clientY' in event) {
const { clientX, clientY } = event;
setDropdownPosition({ x: clientX, y: clientY });
if (!isConnectedRef.current) {
setActiveDropdown('drag');
showModal();
preventCloseRef.current = true;
setTimeout(() => {
preventCloseRef.current = false;
}, 300);
}
}
};
return (
<div className={cn(styles.canvasWrapper, 'px-5 pb-5')}>
<svg
xmlns="http://www.w3.org/2000/svg"
style={{ position: 'absolute', top: 10, left: 0 }}
>
<defs>
<marker
fill="rgb(157 149 225)"
id="logo"
viewBox="0 0 40 40"
refX="8"
refY="5"
markerUnits="strokeWidth"
markerWidth="20"
markerHeight="20"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
</svg>
<AgentInstanceContext.Provider value={{ addCanvasNode, showFormDrawer }}>
<ReactFlow
connectionMode={ConnectionMode.Loose}
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
onEdgesChange={onEdgesChange}
fitView
onConnect={onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
onInit={setReactFlowInstance}
onSelectionChange={onSelectionChange}
nodeOrigin={[0.5, 0]}
isValidConnection={isValidConnection}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseLeave={onEdgeMouseLeave}
className="h-full"
colorMode={theme}
defaultEdgeOptions={{
type: 'buttonEdge',
markerEnd: 'logo',
style: {
strokeWidth: 1,
stroke: isDarkTheme
? 'rgba(91, 93, 106, 1)'
: 'rgba(151, 154, 171, 1)',
},
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
}}
deleteKeyCode={['Delete', 'Backspace']}
onBeforeDelete={handleBeforeDelete}
>
<AgentBackground></AgentBackground>
<Spotlight className="z-0" opcity={0.7} coverage={70} />
<Controls position={'bottom-center'} orientation="horizontal">
<ControlButton>
<Tooltip>
<TooltipTrigger asChild>
<NotebookPen className="!fill-none" onClick={showImage} />
</TooltipTrigger>
<TooltipContent>{t('flow.note')}</TooltipContent>
</Tooltip>
</ControlButton>
</Controls>
</ReactFlow>
{visible && (
<HandleContext.Provider
value={{
nodeId: connectionStartRef.current?.nodeId || '',
id: connectionStartRef.current?.handleId || '',
type: 'source',
position: Position.Right,
isFromConnectionDrag: true,
}}
>
<NextStepDropdown
hideModal={() => {
hideModal();
clearActiveDropdown();
}}
position={dropdownPosition}
nodeId={connectionStartRef.current?.nodeId || ''}
>
<span></span>
</NextStepDropdown>
</HandleContext.Provider>
)}
</AgentInstanceContext.Provider>
<NotebookPen
className={cn('hidden absolute size-6', { block: imgVisible })}
ref={ref}
></NotebookPen>
{formDrawerVisible && (
<AgentInstanceContext.Provider
value={{ addCanvasNode, showFormDrawer }}
>
<FormSheet
node={clickedNode}
visible={formDrawerVisible}
hideModal={hideFormDrawer}
chatVisible={chatVisible}
singleDebugDrawerVisible={singleDebugDrawerVisible}
hideSingleDebugDrawer={hideSingleDebugDrawer}
showSingleDebugDrawer={showSingleDebugDrawer}
></FormSheet>
</AgentInstanceContext.Provider>
)}
{runVisible && (
<RunSheet
hideModal={hideRunOrChatDrawer}
run={run}
loading={running}
></RunSheet>
)}
</div>
);
}
export default memo(DataFlowCanvas);

View File

@ -1,36 +0,0 @@
import { IBeginNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { NodeHandleId, Operator } from '../../constant';
import OperatorIcon from '../../operator-icon';
import { CommonHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import { NodeWrapper } from './node-wrapper';
// TODO: do not allow other nodes to connect to this node
function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
const { t } = useTranslation();
return (
<NodeWrapper selected={selected}>
<CommonHandle
type="source"
position={Position.Right}
isConnectable
style={RightHandleStyle}
nodeId={id}
id={NodeHandleId.Start}
></CommonHandle>
<section className="flex items-center gap-2">
<OperatorIcon name={data.label as Operator}></OperatorIcon>
<div className="truncate text-center font-semibold text-sm">
{t(`dataflow.begin`)}
</div>
</section>
</NodeWrapper>
);
}
export const BeginNode = memo(InnerBeginNode);

View File

@ -1,12 +0,0 @@
import { cn } from '@/lib/utils';
import { PropsWithChildren } from 'react';
type LabelCardProps = {
className?: string;
} & PropsWithChildren;
export function LabelCard({ children, className }: LabelCardProps) {
return (
<div className={cn('bg-bg-card rounded-sm p-1', className)}>{children}</div>
);
}

View File

@ -1,294 +0,0 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { IModalProps } from '@/interfaces/common';
import { useGetNodeDescription, useGetNodeName } from '@/pages/data-flow/hooks';
import useGraphStore from '@/pages/data-flow/store';
import { Position } from '@xyflow/react';
import { t } from 'i18next';
import {
PropsWithChildren,
createContext,
memo,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
} from 'react';
import { Operator } from '../../../constant';
import { AgentInstanceContext, HandleContext } from '../../../context';
import OperatorIcon from '../../../operator-icon';
type OperatorItemProps = {
operators: Operator[];
isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number };
};
const HideModalContext = createContext<IModalProps<any>['showModal']>(() => {});
const OnNodeCreatedContext = createContext<
((newNodeId: string) => void) | undefined
>(undefined);
function OperatorItemList({
operators,
isCustomDropdown = false,
mousePosition,
}: OperatorItemProps) {
const { addCanvasNode } = useContext(AgentInstanceContext);
const handleContext = useContext(HandleContext);
const hideModal = useContext(HideModalContext);
const onNodeCreated = useContext(OnNodeCreatedContext);
const getNodeName = useGetNodeName();
const getNodeDescription = useGetNodeDescription();
const handleClick =
(operator: Operator): React.MouseEventHandler<HTMLElement> =>
(e) => {
const contextData = handleContext || {
nodeId: '',
id: '',
type: 'source' as const,
position: Position.Right,
isFromConnectionDrag: true,
};
const mockEvent = mousePosition
? {
clientX: mousePosition.x,
clientY: mousePosition.y,
}
: e;
const newNodeId = addCanvasNode(operator, contextData)(mockEvent);
if (onNodeCreated && newNodeId) {
onNodeCreated(newNodeId);
}
hideModal?.();
};
const renderOperatorItem = (operator: Operator) => {
const commonContent = (
<div className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start">
<OperatorIcon name={operator} />
{getNodeName(operator)}
</div>
);
return (
<Tooltip key={operator}>
<TooltipTrigger asChild>
{isCustomDropdown ? (
<li onClick={handleClick(operator)}>{commonContent}</li>
) : (
<DropdownMenuItem
key={operator}
className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start"
onClick={handleClick(operator)}
onSelect={() => hideModal?.()}
>
<OperatorIcon name={operator} />
{getNodeName(operator)}
</DropdownMenuItem>
)}
</TooltipTrigger>
<TooltipContent side="right">
<p>{getNodeDescription(operator)}</p>
</TooltipContent>
</Tooltip>
);
};
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
}
// Limit the number of operators of a certain type on the canvas to only one
function useRestrictSingleOperatorOnCanvas() {
const { findNodeByName } = useGraphStore((state) => state);
const restrictSingleOperatorOnCanvas = useCallback(
(singleOperators: Operator[]) => {
const list: Operator[] = [];
singleOperators.forEach((operator) => {
if (!findNodeByName(operator)) {
list.push(operator);
}
});
return list;
},
[findNodeByName],
);
return restrictSingleOperatorOnCanvas;
}
function AccordionOperators({
isCustomDropdown = false,
mousePosition,
nodeId,
}: {
isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number };
nodeId?: string;
}) {
const restrictSingleOperatorOnCanvas = useRestrictSingleOperatorOnCanvas();
const { getOperatorTypeFromId } = useGraphStore((state) => state);
const operators = useMemo(() => {
let list = [
...restrictSingleOperatorOnCanvas([Operator.Parser, Operator.Tokenizer]),
];
list.push(Operator.Extractor);
return list;
}, [restrictSingleOperatorOnCanvas]);
const chunkerOperators = useMemo(() => {
return [
...restrictSingleOperatorOnCanvas([
Operator.Splitter,
Operator.HierarchicalMerger,
]),
];
}, [restrictSingleOperatorOnCanvas]);
const showChunker = useMemo(() => {
return (
getOperatorTypeFromId(nodeId) !== Operator.Extractor &&
chunkerOperators.length > 0
);
}, [chunkerOperators.length, getOperatorTypeFromId, nodeId]);
return (
<>
<OperatorItemList
operators={operators}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
{showChunker && (
<Accordion
type="single"
collapsible
className="w-full px-4"
defaultValue="item-1"
>
<AccordionItem value="item-1">
<AccordionTrigger>Chunker</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<OperatorItemList
operators={chunkerOperators}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}
></OperatorItemList>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
</>
);
}
type NextStepDropdownProps = PropsWithChildren &
IModalProps<any> & {
position?: { x: number; y: number };
onNodeCreated?: (newNodeId: string) => void;
nodeId?: string;
};
export function InnerNextStepDropdown({
children,
hideModal,
position,
onNodeCreated,
nodeId,
}: NextStepDropdownProps) {
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (position && hideModal) {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
hideModal();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}
}, [position, hideModal]);
if (position) {
return (
<div
ref={dropdownRef}
style={{
position: 'fixed',
left: position.x,
top: position.y + 10,
zIndex: 1000,
}}
onClick={(e) => e.stopPropagation()}
>
<div className="w-[300px] font-semibold bg-bg-base border border-border rounded-md shadow-lg">
<div className="px-3 py-2 border-b border-border">
<div className="text-sm font-medium">{t('flow.nextStep')}</div>
</div>
<HideModalContext.Provider value={hideModal}>
<OnNodeCreatedContext.Provider value={onNodeCreated}>
<AccordionOperators
isCustomDropdown={true}
mousePosition={position}
nodeId={nodeId}
></AccordionOperators>
</OnNodeCreatedContext.Provider>
</HideModalContext.Provider>
</div>
</div>
);
}
return (
<DropdownMenu
open={true}
onOpenChange={(open) => {
if (!open && hideModal) {
hideModal();
}
}}
>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent
onClick={(e) => e.stopPropagation()}
className="w-[300px] font-semibold"
>
<DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel>
<HideModalContext.Provider value={hideModal}>
<AccordionOperators nodeId={nodeId}></AccordionOperators>
</HideModalContext.Provider>
</DropdownMenuContent>
</DropdownMenu>
);
}
export const NextStepDropdown = memo(InnerNextStepDropdown);

View File

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

View File

@ -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,72 +0,0 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import { Handle, HandleProps } from '@xyflow/react';
import { Plus } from 'lucide-react';
import { useMemo } from 'react';
import { HandleContext } from '../../context';
import useGraphStore from '../../store';
import { useDropdownManager } from '../context';
import { NextStepDropdown } from './dropdown/next-step-dropdown';
export function CommonHandle({
className,
nodeId,
...props
}: HandleProps & { nodeId: string }) {
const { visible, hideModal, showModal } = useSetModalState();
const { canShowDropdown, setActiveDropdown, clearActiveDropdown } =
useDropdownManager();
const { hasChildNode } = useGraphStore((state) => state);
const value = useMemo(
() => ({
nodeId,
id: props.id || undefined,
type: props.type,
position: props.position,
isFromConnectionDrag: false,
}),
[nodeId, props.id, props.position, props.type],
);
return (
<HandleContext.Provider value={value}>
<Handle
{...props}
className={cn(
'inline-flex justify-center items-center !bg-accent-primary !size-4 !rounded-sm !border-none ',
className,
)}
onClick={(e) => {
e.stopPropagation();
if (hasChildNode(nodeId)) {
return;
}
if (!canShowDropdown()) {
return;
}
setActiveDropdown('handle');
showModal();
}}
>
<Plus className="size-3 pointer-events-none text-text-title-invert" />
{visible && (
<NextStepDropdown
hideModal={() => {
hideModal();
clearActiveDropdown();
}}
nodeId={nodeId}
>
<span></span>
</NextStepDropdown>
)}
</Handle>
</HandleContext.Provider>
);
}

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,56 +0,0 @@
import { IRagNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react';
import { PropsWithChildren, memo, useMemo } from 'react';
import { NodeHandleId, SingleOperators } from '../../constant';
import useGraphStore from '../../store';
import { CommonHandle } from './handle';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar';
type RagNodeProps = NodeProps<IRagNode> & PropsWithChildren;
function InnerRagNode({
id,
data,
isConnectable = true,
selected,
children,
}: RagNodeProps) {
const getOperatorTypeFromId = useGraphStore(
(state) => state.getOperatorTypeFromId,
);
const showCopy = useMemo(() => {
const operatorName = getOperatorTypeFromId(id);
return SingleOperators.every((x) => x !== operatorName);
}, [getOperatorTypeFromId, id]);
return (
<ToolBar selected={selected} id={id} label={data.label} showCopy={showCopy}>
<NodeWrapper selected={selected}>
<CommonHandle
id={NodeHandleId.End}
type="target"
position={Position.Left}
isConnectable={isConnectable}
style={LeftHandleStyle}
nodeId={id}
></CommonHandle>
<CommonHandle
type="source"
position={Position.Right}
isConnectable={isConnectable}
id={NodeHandleId.Start}
style={RightHandleStyle}
nodeId={id}
isConnectableEnd={false}
></CommonHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
{children}
</NodeWrapper>
</ToolBar>
);
}
export const RagNode = memo(InnerRagNode);

View File

@ -1,36 +0,0 @@
import { cn } from '@/lib/utils';
import { memo } from 'react';
import { Operator } from '../../constant';
import OperatorIcon from '../../operator-icon';
interface IProps {
id: string;
label: string;
name: string;
gap?: number;
className?: string;
wrapperClassName?: string;
icon?: React.ReactNode;
}
const InnerNodeHeader = ({
label,
name,
className,
wrapperClassName,
icon,
}: IProps) => {
return (
<section className={cn(wrapperClassName, 'pb-4')}>
<div className={cn(className, 'flex gap-2.5')}>
{icon || <OperatorIcon name={label as Operator}></OperatorIcon>}
<span className="truncate text-center font-semibold text-sm">
{name}
</span>
</div>
</section>
);
};
const NodeHeader = memo(InnerNodeHeader);
export default NodeHeader;

View File

@ -1,18 +0,0 @@
import { cn } from '@/lib/utils';
import { HTMLAttributes } from 'react';
type IProps = HTMLAttributes<HTMLDivElement> & { selected?: boolean };
export function NodeWrapper({ children, className, selected }: IProps) {
return (
<section
className={cn(
'bg-text-title-invert p-2.5 rounded-sm w-[200px] text-xs',
{ 'border border-accent-primary': selected },
className,
)}
>
{children}
</section>
);
}

View File

@ -1,17 +0,0 @@
import { INoteNode } from '@/interfaces/database/flow';
import BaseNoteNode from '@/pages/agent/canvas/node/note-node';
import { NodeProps } from '@xyflow/react';
import { memo } from 'react';
import { useWatchFormChange, useWatchNameFormChange } from './use-watch-change';
function NoteNode({ ...props }: NodeProps<INoteNode>) {
return (
<BaseNoteNode
{...props}
useWatchNoteFormChange={useWatchFormChange}
useWatchNoteNameFormChange={useWatchNameFormChange}
></BaseNoteNode>
);
}
export default memo(NoteNode);

View File

@ -1,30 +0,0 @@
import useGraphStore from '@/pages/data-flow/store';
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id) {
values = form?.getValues() || {};
let nextValues: any = values;
updateNodeForm(id, nextValues);
}
}, [id, updateNodeForm, values]);
}
export function useWatchNameFormChange(id?: string, form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control });
const updateNodeName = useGraphStore((state) => state.updateNodeName);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id) {
updateNodeName(id, values.name);
}
}, [id, updateNodeName, values]);
}

View File

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

View File

@ -1,32 +0,0 @@
export function ResizeIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="rgba(76, 164, 231, 1)"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
style={{
position: 'absolute',
right: 5,
bottom: 5,
}}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="16 20 20 20 20 16" />
<line x1="14" y1="14" x2="20" y2="20" />
<polyline points="8 4 4 4 4 8" />
<line x1="4" y1="4" x2="10" y2="10" />
</svg>
);
}
export const controlStyle = {
background: 'transparent',
border: 'none',
cursor: 'nwse-resize',
};

View File

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

View File

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

View File

@ -1,85 +0,0 @@
import {
TooltipContent,
TooltipNode,
TooltipTrigger,
} from '@/components/xyflow/tooltip-node';
import { Position } from '@xyflow/react';
import { Copy, Play, Trash2 } from 'lucide-react';
import {
HTMLAttributes,
MouseEventHandler,
PropsWithChildren,
useCallback,
} from 'react';
import { useDuplicateNode } from '../../hooks';
import useGraphStore from '../../store';
function IconWrapper({ children, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<div className="p-1.5 bg-text-title rounded-sm cursor-pointer" {...props}>
{children}
</div>
);
}
type ToolBarProps = {
selected?: boolean | undefined;
label: string;
id: string;
showRun?: boolean;
showCopy?: boolean;
} & PropsWithChildren;
export function ToolBar({
selected,
children,
label,
id,
showRun = false,
showCopy = true,
}: ToolBarProps) {
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
const deleteNode: MouseEventHandler<HTMLDivElement> = useCallback(
(e) => {
e.stopPropagation();
deleteNodeById(id);
},
[deleteNodeById, id],
);
const duplicateNode = useDuplicateNode();
const handleDuplicate: MouseEventHandler<HTMLDivElement> = useCallback(
(e) => {
e.stopPropagation();
duplicateNode(id, label);
},
[duplicateNode, id, label],
);
return (
<TooltipNode selected={selected}>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipContent position={Position.Top}>
<section className="flex gap-2 items-center">
{showRun && (
<IconWrapper>
<Play className="size-3.5" data-play />
</IconWrapper>
)}
{showCopy && (
<IconWrapper onClick={handleDuplicate}>
<Copy className="size-3.5" />
</IconWrapper>
)}
<IconWrapper onClick={deleteNode}>
<Trash2 className="size-3.5" />
</IconWrapper>
</section>
</TooltipContent>
</TooltipNode>
);
}

View File

@ -1,324 +0,0 @@
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
import {
initialLlmBaseValues,
DataflowOperator as Operator,
} from '@/constants/agent';
export { DataflowOperator as Operator } from '@/constants/agent';
export enum FileType {
PDF = 'pdf',
Spreadsheet = 'spreadsheet',
Image = 'image',
Email = 'email',
TextMarkdown = 'text&markdown',
Docx = 'word',
PowerPoint = 'slides',
Video = 'video',
Audio = 'audio',
}
export enum PdfOutputFormat {
Json = 'json',
Markdown = 'markdown',
}
export enum SpreadsheetOutputFormat {
Json = 'json',
Html = 'html',
}
export enum ImageOutputFormat {
Text = 'text',
}
export enum EmailOutputFormat {
Json = 'json',
Text = 'text',
}
export enum TextMarkdownOutputFormat {
Text = 'text',
}
export enum DocxOutputFormat {
Markdown = 'markdown',
Json = 'json',
}
export enum PptOutputFormat {
Json = 'json',
}
export enum VideoOutputFormat {
Json = 'json',
}
export enum AudioOutputFormat {
Text = 'text',
}
export const OutputFormatMap = {
[FileType.PDF]: PdfOutputFormat,
[FileType.Spreadsheet]: SpreadsheetOutputFormat,
[FileType.Image]: ImageOutputFormat,
[FileType.Email]: EmailOutputFormat,
[FileType.TextMarkdown]: TextMarkdownOutputFormat,
[FileType.Docx]: DocxOutputFormat,
[FileType.PowerPoint]: PptOutputFormat,
[FileType.Video]: VideoOutputFormat,
[FileType.Audio]: AudioOutputFormat,
};
export const InitialOutputFormatMap = {
[FileType.PDF]: PdfOutputFormat.Json,
[FileType.Spreadsheet]: SpreadsheetOutputFormat.Html,
[FileType.Image]: ImageOutputFormat.Text,
[FileType.Email]: EmailOutputFormat.Text,
[FileType.TextMarkdown]: TextMarkdownOutputFormat.Text,
[FileType.Docx]: DocxOutputFormat.Json,
[FileType.PowerPoint]: PptOutputFormat.Json,
[FileType.Video]: VideoOutputFormat.Json,
[FileType.Audio]: AudioOutputFormat.Text,
};
export enum ContextGeneratorFieldName {
Summary = 'summary',
Keywords = 'keywords',
Questions = 'questions',
Metadata = 'metadata',
}
export const BeginId = 'File';
export const SwitchElseTo = 'end_cpn_ids';
export enum TokenizerSearchMethod {
Embedding = 'embedding',
FullText = 'full_text',
}
export enum ImageParseMethod {
OCR = 'ocr',
}
export enum TokenizerFields {
Text = 'text',
Questions = 'questions',
Summary = 'summary',
}
export enum ParserFields {
From = 'from',
To = 'to',
Cc = 'cc',
Bcc = 'bcc',
Date = 'date',
Subject = 'subject',
Body = 'body',
Attachments = 'attachments',
}
export const initialBeginValues = {
outputs: {
name: {
type: 'string',
value: '',
},
file: {
type: 'Object',
value: {},
},
},
};
export const initialNoteValues = {
text: '',
};
export const initialTokenizerValues = {
search_method: [
TokenizerSearchMethod.Embedding,
TokenizerSearchMethod.FullText,
],
filename_embd_weight: 0.1,
fields: TokenizerFields.Text,
outputs: {},
};
export enum StringTransformMethod {
Merge = 'merge',
Split = 'split',
}
export enum StringTransformDelimiter {
Comma = ',',
Semicolon = ';',
Period = '.',
LineBreak = '\n',
Tab = '\t',
Space = ' ',
}
export const initialParserValues = {
outputs: {
markdown: { type: 'string', value: '' },
text: { type: 'string', value: '' },
html: { type: 'string', value: '' },
json: { type: 'Array<object>', value: [] },
},
setups: [
{
fileFormat: FileType.PDF,
output_format: PdfOutputFormat.Json,
parse_method: ParseDocumentType.DeepDOC,
},
{
fileFormat: FileType.Spreadsheet,
output_format: SpreadsheetOutputFormat.Html,
},
{
fileFormat: FileType.Image,
output_format: ImageOutputFormat.Text,
parse_method: ImageParseMethod.OCR,
system_prompt: '',
},
{
fileFormat: FileType.Email,
fields: Object.values(ParserFields),
output_format: EmailOutputFormat.Text,
},
{
fileFormat: FileType.TextMarkdown,
output_format: TextMarkdownOutputFormat.Text,
},
{
fileFormat: FileType.Docx,
output_format: DocxOutputFormat.Json,
},
{
fileFormat: FileType.PowerPoint,
output_format: PptOutputFormat.Json,
},
],
};
export const initialSplitterValues = {
outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
chunk_token_size: 512,
overlapped_percent: 0,
delimiters: [{ value: '\n' }],
};
export enum Hierarchy {
H1 = '1',
H2 = '2',
H3 = '3',
H4 = '4',
H5 = '5',
}
export const initialHierarchicalMergerValues = {
outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
hierarchy: Hierarchy.H3,
levels: [
{ expressions: [{ expression: '^#[^#]' }] },
{ expressions: [{ expression: '^##[^#]' }] },
{ expressions: [{ expression: '^###[^#]' }] },
{ expressions: [{ expression: '^####[^#]' }] },
],
};
export const initialExtractorValues = {
...initialLlmBaseValues,
field_name: ContextGeneratorFieldName.Summary,
outputs: {
chunks: { type: 'Array<Object>', value: [] },
},
};
export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 },
{ top: 8, right: 18 },
{ top: 15, right: 10 },
{ top: 24, right: 4 },
{ top: 31, right: 1 },
{ top: 38, right: -2 },
{ top: 62, right: -2 }, //bottom
{ top: 71, right: 1 },
{ top: 79, right: 6 },
{ top: 86, right: 12 },
{ top: 91, right: 20 },
{ top: 98, right: 34 },
];
// key is the source of the edge, value is the target of the edge
// no connection lines are allowed between key and value
export const RestrictedUpstreamMap: Record<Operator, Operator[]> = {
[Operator.Begin]: [] as Operator[],
[Operator.Parser]: [Operator.Begin],
[Operator.Splitter]: [Operator.Begin],
[Operator.HierarchicalMerger]: [Operator.Begin],
[Operator.Tokenizer]: [Operator.Begin],
[Operator.Extractor]: [Operator.Begin],
[Operator.Note]: [Operator.Begin],
};
export const NodeMap = {
[Operator.Begin]: 'beginNode',
[Operator.Note]: 'noteNode',
[Operator.Parser]: 'parserNode',
[Operator.Tokenizer]: 'tokenizerNode',
[Operator.Splitter]: 'splitterNode',
[Operator.HierarchicalMerger]: 'splitterNode',
[Operator.Extractor]: 'contextNode',
};
export const NoDebugOperatorsList = [Operator.Begin];
export enum NodeHandleId {
Start = 'start',
End = 'end',
Tool = 'tool',
AgentTop = 'agentTop',
AgentBottom = 'agentBottom',
AgentException = 'agentException',
}
export const FileTypeSuffixMap = {
[FileType.PDF]: ['pdf'],
[FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'],
[FileType.Image]: ['jpg', 'jpeg', 'png', 'gif'],
[FileType.Email]: ['eml', 'msg'],
[FileType.TextMarkdown]: ['md', 'markdown', 'mdx', 'txt'],
[FileType.Docx]: ['doc', 'docx'],
[FileType.PowerPoint]: ['pptx'],
[FileType.Video]: [],
[FileType.Audio]: [
'da',
'wave',
'wav',
'mp3',
'aac',
'flac',
'ogg',
'aiff',
'au',
'midi',
'wma',
'realaudio',
'vqf',
'oggvorbis',
'ape',
],
};
export const SingleOperators = [
Operator.Tokenizer,
Operator.Splitter,
Operator.HierarchicalMerger,
Operator.Parser,
];

View File

@ -1,39 +0,0 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { HandleType, Position } from '@xyflow/react';
import { createContext } from 'react';
import { useAddNode } from './hooks/use-add-node';
import { useShowFormDrawer } from './hooks/use-show-drawer';
export const AgentFormContext = createContext<RAGFlowNodeType | undefined>(
undefined,
);
type AgentInstanceContextType = Pick<
ReturnType<typeof useAddNode>,
'addCanvasNode'
> &
Pick<ReturnType<typeof useShowFormDrawer>, 'showFormDrawer'>;
export const AgentInstanceContext = createContext<AgentInstanceContextType>(
{} as AgentInstanceContextType,
);
export type HandleContextType = {
nodeId?: string;
id?: string;
type: HandleType;
position: Position;
isFromConnectionDrag: boolean;
};
export const HandleContext = createContext<HandleContextType>(
{} as HandleContextType,
);
export type LogContextType = {
messageId: string;
setMessageId: (messageId: string) => void;
setUploadedFileData: (data: Record<string, any>) => void;
};
export const LogContext = createContext<LogContextType>({} as LogContextType);

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,30 +0,0 @@
import { Operator } from '../constant';
import ExtractorForm from '../form/extractor-form';
import HierarchicalMergerForm from '../form/hierarchical-merger-form';
import ParserForm from '../form/parser-form';
import SplitterForm from '../form/splitter-form';
import TokenizerForm from '../form/tokenizer-form';
export const FormConfigMap = {
[Operator.Begin]: {
component: () => <></>,
},
[Operator.Note]: {
component: () => <></>,
},
[Operator.Parser]: {
component: ParserForm,
},
[Operator.Tokenizer]: {
component: TokenizerForm,
},
[Operator.Splitter]: {
component: SplitterForm,
},
[Operator.HierarchicalMerger]: {
component: HierarchicalMergerForm,
},
[Operator.Extractor]: {
component: ExtractorForm,
},
};

View File

@ -1,104 +0,0 @@
import { Input } from '@/components/ui/input';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { IModalProps } from '@/interfaces/common';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { BeginId, Operator } from '../constant';
import { AgentFormContext } from '../context';
import { useHandleNodeNameChange } from '../hooks/use-change-node-name';
import OperatorIcon from '../operator-icon';
import { FormConfigMap } from './form-config-map';
interface IProps {
node?: RAGFlowNodeType;
singleDebugDrawerVisible: IModalProps<any>['visible'];
hideSingleDebugDrawer: IModalProps<any>['hideModal'];
showSingleDebugDrawer: IModalProps<any>['showModal'];
chatVisible: boolean;
}
const EmptyContent = () => <div></div>;
const FormSheet = ({
visible,
hideModal,
node,
chatVisible,
}: IModalProps<any> & IProps) => {
const operatorName: Operator = node?.data.label as Operator;
// const clickedToolId = useGraphStore((state) => state.clickedToolId);
const currentFormMap = FormConfigMap[operatorName];
const OperatorForm = currentFormMap?.component ?? EmptyContent;
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
id: node?.id,
data: node?.data,
});
const { t } = useTranslation();
return (
<Sheet open={visible} modal={false}>
<SheetContent
className={cn('top-20 p-0 flex flex-col pb-20 ', {
'right-[620px]': chatVisible,
})}
closeIcon={false}
>
<SheetHeader>
<SheetTitle className="hidden"></SheetTitle>
<section className="flex-col border-b py-2 px-5">
<div className="flex items-center gap-2 pb-3">
<OperatorIcon
name={
operatorName === Operator.HierarchicalMerger
? Operator.Splitter
: operatorName
}
></OperatorIcon>
<div className="flex items-center gap-1 flex-1">
<label htmlFor="">{t('flow.title')}</label>
{node?.id === BeginId ? (
<span>{t(BeginId)}</span>
) : (
<Input
value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
)}
</div>
{/* {needsSingleStepDebugging(operatorName) && (
<RunTooltip>
<Play
className="size-5 cursor-pointer"
onClick={showSingleDebugDrawer}
/>
</RunTooltip>
)} */}
<X onClick={hideModal} />
</div>
</section>
</SheetHeader>
<section className="pt-4 overflow-auto flex-1">
{visible && (
<AgentFormContext.Provider value={node}>
<OperatorForm node={node} key={node?.id}></OperatorForm>
</AgentFormContext.Provider>
)}
</section>
</SheetContent>
</Sheet>
);
};
export default FormSheet;

View File

@ -1,16 +0,0 @@
type FormProps = React.ComponentProps<'form'>;
export function FormWrapper({ children, ...props }: FormProps) {
return (
<form
className="space-y-6 p-4"
autoComplete="off"
onSubmit={(e) => {
e.preventDefault();
}}
{...props}
>
{children}
</form>
);
}

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,35 +0,0 @@
import { t } from 'i18next';
export type OutputType = {
title: string;
type?: string;
};
type OutputProps = {
list: Array<OutputType>;
};
export function transferOutputs(outputs: Record<string, any>) {
return Object.entries(outputs).map(([key, value]) => ({
title: key,
type: value?.type,
}));
}
export function Output({ list }: OutputProps) {
return (
<section className="space-y-2">
<div>{t('flow.output')}</div>
<ul>
{list.map((x, idx) => (
<li
key={idx}
className="bg-background-highlight text-accent-primary rounded-sm px-2 py-1"
>
{x.title}: <span className="text-text-secondary">{x.type}</span>
</li>
))}
</ul>
</section>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,144 +0,0 @@
import { Connection, Edge, getOutgoers } from '@xyflow/react';
import { useCallback } from 'react';
// import { shallow } from 'zustand/shallow';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { lowerFirst } from 'lodash';
import { useTranslation } from 'react-i18next';
import { Operator, RestrictedUpstreamMap } from './constant';
import useGraphStore, { RFState } from './store';
import { replaceIdWithText } from './utils';
const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
onConnect: state.onConnect,
setNodes: state.setNodes,
onSelectionChange: state.onSelectionChange,
onEdgeMouseEnter: state.onEdgeMouseEnter,
onEdgeMouseLeave: state.onEdgeMouseLeave,
});
export const useSelectCanvasData = () => {
// return useStore(useShallow(selector)); // throw error
// return useStore(selector, shallow);
return useGraphStore(selector);
};
export const useGetNodeName = () => {
const { t } = useTranslation();
return (type: string) => {
const name = t(`dataflow.${lowerFirst(type)}`);
return name;
};
};
export const useGetNodeDescription = () => {
const { t } = useTranslation();
return (type: string) => {
const name = t(`dataflow.${lowerFirst(type)}Description`);
return name;
};
};
export const useValidateConnection = () => {
const { getOperatorTypeFromId, getParentIdById, edges, nodes } =
useGraphStore((state) => state);
const isSameNodeChild = useCallback(
(connection: Connection | Edge) => {
const sourceParentId = getParentIdById(connection.source);
const targetParentId = getParentIdById(connection.target);
if (sourceParentId || targetParentId) {
return sourceParentId === targetParentId;
}
return true;
},
[getParentIdById],
);
const hasCanvasCycle = useCallback(
(connection: Connection | Edge) => {
const target = nodes.find((node) => node.id === connection.target);
const hasCycle = (node: RAGFlowNodeType, visited = new Set()) => {
if (visited.has(node.id)) return false;
visited.add(node.id);
for (const outgoer of getOutgoers(node, nodes, edges)) {
if (outgoer.id === connection.source) return true;
if (hasCycle(outgoer, visited)) return true;
}
};
if (target?.id === connection.source) return false;
return target ? !hasCycle(target) : false;
},
[edges, nodes],
);
// restricted lines cannot be connected successfully.
const isValidConnection = useCallback(
(connection: Connection | Edge) => {
// node cannot connect to itself
const isSelfConnected = connection.target === connection.source;
// limit the connection between two nodes to only one connection line in one direction
// const hasLine = edges.some(
// (x) => x.source === connection.source && x.target === connection.target,
// );
const ret =
!isSelfConnected &&
RestrictedUpstreamMap[
getOperatorTypeFromId(connection.source) as Operator
]?.every((x) => x !== getOperatorTypeFromId(connection.target)) &&
isSameNodeChild(connection) &&
hasCanvasCycle(connection);
return ret;
},
[getOperatorTypeFromId, hasCanvasCycle, isSameNodeChild],
);
return isValidConnection;
};
export const useReplaceIdWithName = () => {
const getNode = useGraphStore((state) => state.getNode);
const replaceIdWithName = useCallback(
(id?: string) => {
return getNode(id)?.data.name;
},
[getNode],
);
return replaceIdWithName;
};
export const useReplaceIdWithText = (output: unknown) => {
const getNameById = useReplaceIdWithName();
return {
replacedOutput: replaceIdWithText(output, getNameById),
getNameById,
};
};
export const useDuplicateNode = () => {
const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
const getNodeName = useGetNodeName();
const duplicateNode = useCallback(
(id: string, label: string) => {
duplicateNodeById(id, getNodeName(label));
},
[duplicateNodeById, getNodeName],
);
return duplicateNode;
};

View File

@ -1,218 +0,0 @@
import { useFetchModelId } from '@/hooks/logic-hooks';
import { Connection, Node, Position, ReactFlowInstance } from '@xyflow/react';
import humanId from 'human-id';
import { lowerFirst } from 'lodash';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
NodeHandleId,
NodeMap,
Operator,
initialBeginValues,
initialExtractorValues,
initialHierarchicalMergerValues,
initialNoteValues,
initialParserValues,
initialSplitterValues,
initialTokenizerValues,
} from '../constant';
import useGraphStore from '../store';
import {
generateNodeNamesWithIncreasingIndex,
getNodeDragHandle,
} from '../utils';
export const useInitializeOperatorParams = () => {
const llmId = useFetchModelId();
const { t } = useTranslation();
const initialFormValuesMap = useMemo(() => {
return {
[Operator.Begin]: initialBeginValues,
[Operator.Note]: initialNoteValues,
[Operator.Parser]: initialParserValues,
[Operator.Tokenizer]: initialTokenizerValues,
[Operator.Splitter]: initialSplitterValues,
[Operator.HierarchicalMerger]: initialHierarchicalMergerValues,
[Operator.Extractor]: {
...initialExtractorValues,
llm_id: llmId,
sys_prompt: t('dataflow.prompts.system.summary'),
prompts: t('dataflow.prompts.user.summary'),
},
};
}, [llmId, t]);
const initializeOperatorParams = useCallback(
(operatorName: Operator) => {
const initialValues = initialFormValuesMap[operatorName];
return initialValues;
},
[initialFormValuesMap],
);
return { initializeOperatorParams, initialFormValuesMap };
};
export const useGetNodeName = () => {
const { t } = useTranslation();
return (type: string) => {
const name = t(`dataflow.${lowerFirst(type)}`);
return name;
};
};
export function useCalculateNewlyChildPosition() {
const getNode = useGraphStore((state) => state.getNode);
const nodes = useGraphStore((state) => state.nodes);
const edges = useGraphStore((state) => state.edges);
const calculateNewlyBackChildPosition = useCallback(
(id?: string, sourceHandle?: string) => {
const parentNode = getNode(id);
// Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes
const allChildNodeIds = edges
.filter((x) => x.source === id && x.sourceHandle === sourceHandle)
.map((x) => x.target);
const yAxises = nodes
.filter((x) => allChildNodeIds.some((y) => y === x.id))
.map((x) => x.position.y);
const maxY = Math.max(...yAxises);
const position = {
y: yAxises.length > 0 ? maxY + 150 : parentNode?.position.y || 0,
x: (parentNode?.position.x || 0) + 300,
};
return position;
},
[edges, getNode, nodes],
);
return { calculateNewlyBackChildPosition };
}
function useAddChildEdge() {
const addEdge = useGraphStore((state) => state.addEdge);
const addChildEdge = useCallback(
(position: Position = Position.Right, edge: Partial<Connection>) => {
if (
position === Position.Right &&
edge.source &&
edge.target &&
edge.sourceHandle
) {
addEdge({
source: edge.source,
target: edge.target,
sourceHandle: edge.sourceHandle,
targetHandle: NodeHandleId.End,
});
}
},
[addEdge],
);
return { addChildEdge };
}
type CanvasMouseEvent = Pick<
React.MouseEvent<HTMLElement>,
'clientX' | 'clientY'
>;
export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
const { nodes, addNode } = useGraphStore((state) => state);
const getNodeName = useGetNodeName();
const { initializeOperatorParams } = useInitializeOperatorParams();
const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition();
const { addChildEdge } = useAddChildEdge();
// const [reactFlowInstance, setReactFlowInstance] =
// useState<ReactFlowInstance<any, any>>();
const addCanvasNode = useCallback(
(
type: string,
params: {
nodeId?: string;
position: Position;
id?: string;
isFromConnectionDrag?: boolean;
} = {
position: Position.Right,
},
) =>
(event?: CanvasMouseEvent): string | undefined => {
const nodeId = params.nodeId;
// reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
// and you don't need to subtract the reactFlowBounds.left/top anymore
// details: https://@xyflow/react.dev/whats-new/2023-11-10
let position = reactFlowInstance?.screenToFlowPosition({
x: event?.clientX || 0,
y: event?.clientY || 0,
});
if (
params.position === Position.Right &&
type !== Operator.Note &&
!params.isFromConnectionDrag
) {
position = calculateNewlyBackChildPosition(nodeId, params.id);
}
const newNode: Node<any> = {
id: `${type}:${humanId()}`,
type: NodeMap[type as Operator] || 'ragNode',
position: position || {
x: 0,
y: 0,
},
data: {
label: `${type}`,
name: generateNodeNamesWithIncreasingIndex(
getNodeName(type),
nodes,
),
form: initializeOperatorParams(type as Operator),
},
sourcePosition: Position.Right,
targetPosition: Position.Left,
dragHandle: getNodeDragHandle(type),
};
addNode(newNode);
addChildEdge(params.position, {
source: params.nodeId,
target: newNode.id,
sourceHandle: params.id,
});
return newNode.id;
},
[
addChildEdge,
addNode,
calculateNewlyBackChildPosition,
getNodeName,
initializeOperatorParams,
nodes,
reactFlowInstance,
],
);
const addNoteNode = useCallback(
(e: CanvasMouseEvent) => {
addCanvasNode(Operator.Note)(e);
},
[addCanvasNode],
);
return { addCanvasNode, addNoteNode };
}

View File

@ -1,49 +0,0 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { OnBeforeDelete } from '@xyflow/react';
import { Operator } from '../constant';
import useGraphStore from '../store';
const UndeletableNodes = [Operator.Begin];
export function useBeforeDelete() {
const { getOperatorTypeFromId } = useGraphStore((state) => state);
const handleBeforeDelete: OnBeforeDelete<RAGFlowNodeType> = async ({
nodes, // Nodes to be deleted
edges, // Edges to be deleted
}) => {
const toBeDeletedNodes = nodes.filter((node) => {
const operatorType = node.data?.label as Operator;
if (operatorType === Operator.Begin) {
return false;
}
return true;
});
const toBeDeletedEdges = edges.filter((edge) => {
const sourceType = getOperatorTypeFromId(edge.source) as Operator;
const downStreamNodes = nodes.filter((x) => x.id === edge.target);
// This edge does not need to be deleted, the range of edges that do not need to be deleted is smaller, so consider the case where it does not need to be deleted
if (
UndeletableNodes.includes(sourceType) && // Upstream node is Begin or IterationStart
downStreamNodes.length === 0 // Downstream node does not exist in the nodes to be deleted
) {
if (!nodes.some((x) => x.id === edge.source)) {
return true; // Can be deleted
}
return false; // Cannot be deleted
}
return true;
});
return {
nodes: toBeDeletedNodes,
edges: toBeDeletedEdges,
};
};
return { handleBeforeDelete };
}

View File

@ -1,29 +0,0 @@
import { useFetchAgent } from '@/hooks/use-agent-request';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { useCallback } from 'react';
import useGraphStore from '../store';
import { buildDslComponentsByGraph } from '../utils';
export const useBuildDslData = () => {
const { data } = useFetchAgent();
const { nodes, edges } = useGraphStore((state) => state);
const buildDslData = useCallback(
(currentNodes?: RAGFlowNodeType[]) => {
const dslComponents = buildDslComponentsByGraph(
currentNodes ?? nodes,
edges,
data.dsl.components,
);
return {
...data.dsl,
graph: { nodes: currentNodes ?? nodes, edges },
components: dslComponents,
};
},
[data.dsl, edges, nodes],
);
return { buildDslData };
};

View File

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

View File

@ -1,88 +0,0 @@
import {
IEventList,
INodeEvent,
MessageEventType,
} from '@/hooks/use-send-message';
import { useCallback, useEffect, useMemo, useState } from 'react';
export const ExcludeTypes = [
MessageEventType.Message,
MessageEventType.MessageEnd,
];
export function useCacheChatLog() {
const [eventList, setEventList] = useState<IEventList>([]);
const [messageIdPool, setMessageIdPool] = useState<
Record<string, IEventList>
>({});
const [currentMessageId, setCurrentMessageId] = useState('');
useEffect(() => {
setMessageIdPool((prev) => ({ ...prev, [currentMessageId]: eventList }));
}, [currentMessageId, eventList]);
const filterEventListByMessageId = useCallback(
(messageId: string) => {
return messageIdPool[messageId]?.filter(
(x) => x.message_id === messageId,
);
},
[messageIdPool],
);
const filterEventListByEventType = useCallback(
(eventType: string) => {
return messageIdPool[currentMessageId]?.filter(
(x) => x.event === eventType,
);
},
[messageIdPool, currentMessageId],
);
const clearEventList = useCallback(() => {
setEventList([]);
setMessageIdPool({});
}, []);
const addEventList = useCallback((events: IEventList, message_id: string) => {
setEventList((x) => {
const list = [...x, ...events];
setMessageIdPool((prev) => ({ ...prev, [message_id]: list }));
return list;
});
}, []);
const currentEventListWithoutMessage = useMemo(() => {
const list = messageIdPool[currentMessageId]?.filter(
(x) =>
x.message_id === currentMessageId &&
ExcludeTypes.every((y) => y !== x.event),
);
return list as INodeEvent[];
}, [currentMessageId, messageIdPool]);
const currentEventListWithoutMessageById = useCallback(
(messageId: string) => {
const list = messageIdPool[messageId]?.filter(
(x) =>
x.message_id === messageId &&
ExcludeTypes.every((y) => y !== x.event),
);
return list as INodeEvent[];
},
[messageIdPool],
);
return {
eventList,
currentEventListWithoutMessage,
currentEventListWithoutMessageById,
setEventList,
clearEventList,
addEventList,
filterEventListByEventType,
filterEventListByMessageId,
setCurrentMessageId,
currentMessageId,
};
}

View File

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

View File

@ -1,116 +0,0 @@
import message from '@/components/ui/message';
import { trim } from 'lodash';
import {
ChangeEvent,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import useGraphStore from '../store';
import { getAgentNodeTools } from '../utils';
export function useHandleTooNodeNameChange({
id,
name,
setName,
}: {
id?: string;
name?: string;
setName: Dispatch<SetStateAction<string>>;
}) {
const { clickedToolId, findUpstreamNodeById, updateNodeForm } = useGraphStore(
(state) => state,
);
const agentNode = findUpstreamNodeById(id);
const tools = getAgentNodeTools(agentNode);
const previousName = useMemo(() => {
const tool = tools.find((x) => x.component_name === clickedToolId);
return tool?.name || tool?.component_name;
}, [clickedToolId, tools]);
const handleToolNameBlur = useCallback(() => {
const trimmedName = trim(name);
const existsSameName = tools.some((x) => x.name === trimmedName);
if (trimmedName === '' || existsSameName) {
if (existsSameName && previousName !== name) {
message.error('The name cannot be repeated');
}
setName(previousName || '');
return;
}
if (agentNode?.id) {
const nextTools = tools.map((x) => {
if (x.component_name === clickedToolId) {
return {
...x,
name,
};
}
return x;
});
updateNodeForm(agentNode?.id, nextTools, ['tools']);
}
}, [
agentNode?.id,
clickedToolId,
name,
previousName,
setName,
tools,
updateNodeForm,
]);
return { handleToolNameBlur, previousToolName: previousName };
}
export const useHandleNodeNameChange = ({
id,
data,
}: {
id?: string;
data: any;
}) => {
const [name, setName] = useState<string>('');
const { updateNodeName, nodes } = useGraphStore((state) => state);
const previousName = data?.name;
const { previousToolName } = useHandleTooNodeNameChange({
id,
name,
setName,
});
const handleNameBlur = useCallback(() => {
const existsSameName = nodes.some((x) => x.data.name === name);
if (trim(name) === '' || existsSameName) {
if (existsSameName && previousName !== name) {
message.error('The name cannot be repeated');
}
setName(previousName);
return;
}
if (id) {
updateNodeName(id, name);
}
}, [name, id, updateNodeName, previousName, nodes]);
const handleNameChange = useCallback((e: ChangeEvent<any>) => {
setName(e.target.value);
}, []);
useEffect(() => {
setName(previousName);
}, [previousName, previousToolName]);
return {
name,
handleNameBlur: handleNameBlur,
handleNameChange,
};
};

View File

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

View File

@ -1,17 +0,0 @@
import { useFetchAgent } from '@/hooks/use-agent-request';
import { downloadJsonFile } from '@/utils/file-util';
import { useCallback } from 'react';
import { useBuildDslData } from './use-build-dsl';
export const useHandleExportOrImportJsonFile = () => {
const { buildDslData } = useBuildDslData();
const { data } = useFetchAgent();
const handleExportJson = useCallback(() => {
downloadJsonFile(buildDslData().graph, `${data.title}.json`);
}, [buildDslData, data.title]);
return {
handleExportJson,
};
};

View File

@ -1,19 +0,0 @@
import { useFetchAgent } from '@/hooks/use-agent-request';
import { IGraph } from '@/interfaces/database/flow';
import { useEffect } from 'react';
import { useSetGraphInfo } from './use-set-graph';
export const useFetchDataOnMount = () => {
const { loading, data, refetch } = useFetchAgent();
const setGraphInfo = useSetGraphInfo();
useEffect(() => {
setGraphInfo(data?.dsl?.graph ?? ({} as IGraph));
}, [setGraphInfo, data]);
useEffect(() => {
refetch();
}, [refetch]);
return { loading, flowDetail: data };
};

View File

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

View File

@ -1,20 +0,0 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
export function useFormValues(
defaultValues: Record<string, any>,
node?: RAGFlowNodeType,
) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return defaultValues;
}
return formData;
}, [defaultValues, node?.data?.form]);
return values;
}

View File

@ -1,35 +0,0 @@
import { useMouse } from 'ahooks';
import { useCallback, useEffect, useRef, useState } from 'react';
export function useMoveNote() {
const ref = useRef<SVGSVGElement>(null);
const mouse = useMouse();
const [imgVisible, setImgVisible] = useState(false);
const toggleVisible = useCallback((visible: boolean) => {
setImgVisible(visible);
}, []);
const showImage = useCallback(() => {
toggleVisible(true);
}, [toggleVisible]);
const hideImage = useCallback(() => {
toggleVisible(false);
}, [toggleVisible]);
useEffect(() => {
if (ref.current) {
ref.current.style.top = `${mouse.clientY - 70}px`;
ref.current.style.left = `${mouse.clientX + 10}px`;
}
}, [mouse.clientX, mouse.clientY]);
return {
ref,
showImage,
hideImage,
mouse,
imgVisible,
};
}

View File

@ -1,59 +0,0 @@
import message from '@/components/ui/message';
import { useSendMessageBySSE } from '@/hooks/use-send-message';
import api from '@/utils/api';
import { get } from 'lodash';
import { useCallback, useContext } from 'react';
import { useParams } from 'umi';
import { LogContext } from '../context';
import { useSaveGraphBeforeOpeningDebugDrawer } from './use-save-graph';
export function useRunDataflow(
showLogSheet: () => void,
hideRunOrChatDrawer: () => void,
) {
const { send } = useSendMessageBySSE(api.runCanvas);
const { id } = useParams();
const { setMessageId, setUploadedFileData } = useContext(LogContext);
const { handleRun: saveGraph, loading } =
useSaveGraphBeforeOpeningDebugDrawer(showLogSheet!);
const run = useCallback(
async (fileResponseData: Record<string, any>) => {
const success = await saveGraph();
if (!success) return;
const res = await send({
id,
query: '',
session_id: null,
files: [fileResponseData.file],
});
if (res && res?.response.status === 200 && get(res, 'data.code') === 0) {
// fetch canvas
hideRunOrChatDrawer();
setUploadedFileData(fileResponseData.file);
const msgId = get(res, 'data.data.message_id');
if (msgId) {
setMessageId(msgId);
}
return msgId;
} else {
message.error(get(res, 'data.message', ''));
}
},
[
hideRunOrChatDrawer,
id,
saveGraph,
send,
setMessageId,
setUploadedFileData,
],
);
return { run, loading: loading };
}
export type RunDataflowType = ReturnType<typeof useRunDataflow>;

View File

@ -1,87 +0,0 @@
import { useFetchAgent, useSetAgent } from '@/hooks/use-agent-request';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { formatDate } from '@/utils/date';
import { useDebounceEffect } from 'ahooks';
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'umi';
import useGraphStore from '../store';
import { useBuildDslData } from './use-build-dsl';
export const useSaveGraph = (showMessage: boolean = true) => {
const { data } = useFetchAgent();
const { setAgent, loading } = useSetAgent(showMessage);
const { id } = useParams();
const { buildDslData } = useBuildDslData();
const saveGraph = useCallback(
async (currentNodes?: RAGFlowNodeType[]) => {
return setAgent({
id,
title: data.title,
canvas_category: data.canvas_category,
dsl: buildDslData(currentNodes),
});
},
[setAgent, data, id, buildDslData],
);
return { saveGraph, loading };
};
export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => {
const { saveGraph, loading } = useSaveGraph();
// const { resetAgent } = useResetAgent();
const handleRun = useCallback(
async (nextNodes?: RAGFlowNodeType[]) => {
const saveRet = await saveGraph(nextNodes);
if (saveRet?.code === 0) {
// Call the reset api before opening the run drawer each time
// const resetRet = await resetAgent();
// After resetting, all previous messages will be cleared.
// if (resetRet?.code === 0) {
show();
// }
}
return saveRet?.code === 0;
},
[saveGraph, show],
);
return { handleRun, loading };
};
export const useWatchAgentChange = (chatDrawerVisible: boolean) => {
const [time, setTime] = useState<string>();
const nodes = useGraphStore((state) => state.nodes);
const edges = useGraphStore((state) => state.edges);
const { saveGraph } = useSaveGraph(false);
const { data: flowDetail } = useFetchAgent();
const setSaveTime = useCallback((updateTime: number) => {
setTime(formatDate(updateTime));
}, []);
useEffect(() => {
setSaveTime(flowDetail?.update_time);
}, [flowDetail, setSaveTime]);
const saveAgent = useCallback(async () => {
if (!chatDrawerVisible) {
const ret = await saveGraph();
setSaveTime(ret.data.update_time);
}
}, [chatDrawerVisible, saveGraph, setSaveTime]);
useDebounceEffect(
() => {
saveAgent();
},
[nodes, edges],
{
wait: 1000 * 20,
},
);
return time;
};

View File

@ -1,17 +0,0 @@
import { IGraph } from '@/interfaces/database/flow';
import { useCallback } from 'react';
import useGraphStore from '../store';
export const useSetGraphInfo = () => {
const { setEdges, setNodes } = useGraphStore((state) => state);
const setGraphInfo = useCallback(
({ nodes = [], edges = [] }: IGraph) => {
if (nodes.length || edges.length) {
setNodes(nodes);
setEdges(edges);
}
},
[setEdges, setNodes],
);
return setGraphInfo;
};

View File

@ -1,147 +0,0 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { NodeMouseHandler } from '@xyflow/react';
import get from 'lodash/get';
import React, { useCallback, useEffect } from 'react';
import { BeginId, Operator } from '../constant';
import useGraphStore from '../store';
import { useSaveGraph } from './use-save-graph';
export const useShowFormDrawer = () => {
const {
clickedNodeId: clickNodeId,
setClickedNodeId,
getNode,
} = useGraphStore((state) => state);
const {
visible: formDrawerVisible,
hideModal: hideFormDrawer,
showModal: showFormDrawer,
} = useSetModalState();
const handleShow = useCallback(
(e: React.MouseEvent<Element>, nodeId: string) => {
// TODO: Operator type judgment should be used
if (nodeId === BeginId) {
return;
}
setClickedNodeId(nodeId);
showFormDrawer();
},
[setClickedNodeId, showFormDrawer],
);
return {
formDrawerVisible,
hideFormDrawer,
showFormDrawer: handleShow,
clickedNode: getNode(clickNodeId),
};
};
export const useShowSingleDebugDrawer = () => {
const { visible, showModal, hideModal } = useSetModalState();
const { saveGraph } = useSaveGraph();
const showSingleDebugDrawer = useCallback(async () => {
const saveRet = await saveGraph();
if (saveRet?.code === 0) {
showModal();
}
}, [saveGraph, showModal]);
return {
singleDebugDrawerVisible: visible,
hideSingleDebugDrawer: hideModal,
showSingleDebugDrawer,
};
};
const ExcludedNodes = [Operator.Note];
export function useShowDrawer({
drawerVisible,
hideDrawer,
}: {
drawerVisible: boolean;
hideDrawer(): void;
}) {
const {
visible: runVisible,
showModal: showRunModal,
hideModal: hideRunModal,
} = useSetModalState();
const {
visible: chatVisible,
showModal: showChatModal,
hideModal: hideChatModal,
} = useSetModalState();
const {
singleDebugDrawerVisible,
showSingleDebugDrawer,
hideSingleDebugDrawer,
} = useShowSingleDebugDrawer();
const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } =
useShowFormDrawer();
useEffect(() => {
if (drawerVisible) {
showRunModal();
}
}, [hideChatModal, hideRunModal, showChatModal, showRunModal, drawerVisible]);
const hideRunOrChatDrawer = useCallback(() => {
hideChatModal();
hideRunModal();
hideDrawer();
}, [hideChatModal, hideDrawer, hideRunModal]);
const onPaneClick = useCallback(() => {
hideFormDrawer();
}, [hideFormDrawer]);
const onNodeClick: NodeMouseHandler = useCallback(
(e, node) => {
if (!ExcludedNodes.some((x) => x === node.data.label)) {
hideSingleDebugDrawer();
// hideRunOrChatDrawer();
showFormDrawer(e, node.id);
}
// handle single debug icon click
if (
get(e.target, 'dataset.play') === 'true' ||
get(e.target, 'parentNode.dataset.play') === 'true'
) {
showSingleDebugDrawer();
}
},
[hideSingleDebugDrawer, showFormDrawer, showSingleDebugDrawer],
);
return {
chatVisible,
runVisible,
onPaneClick,
singleDebugDrawerVisible,
showSingleDebugDrawer,
hideSingleDebugDrawer,
formDrawerVisible,
showFormDrawer,
clickedNode,
onNodeClick,
hideFormDrawer,
hideRunOrChatDrawer,
showChatModal,
};
}
export function useHideFormSheetOnNodeDeletion({
hideFormDrawer,
}: Pick<ReturnType<typeof useShowFormDrawer>, 'hideFormDrawer'>) {
const { nodes, clickedNodeId } = useGraphStore((state) => state);
useEffect(() => {
if (!nodes.some((x) => x.id === clickedNodeId)) {
hideFormDrawer();
}
}, [clickedNodeId, hideFormDrawer, nodes]);
}

View File

@ -1,15 +0,0 @@
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import useGraphStore from '../store';
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
if (id) {
updateNodeForm(id, values);
}
}, [id, updateNodeForm, values]);
}

View File

@ -1,232 +0,0 @@
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { Button, ButtonLoading } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import message from '@/components/ui/message';
import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { ReactFlowProvider } from '@xyflow/react';
import {
ChevronDown,
CirclePlay,
History,
LaptopMinimalCheck,
Settings,
Upload,
} from 'lucide-react';
import { ComponentPropsWithoutRef, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import DataFlowCanvas from './canvas';
import { DropdownProvider } from './canvas/context';
import { Operator } from './constant';
import { LogContext } from './context';
import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow';
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useFetchLog } from './hooks/use-fetch-log';
import {
useSaveGraph,
useSaveGraphBeforeOpeningDebugDrawer,
useWatchAgentChange,
} from './hooks/use-save-graph';
import { LogSheet } from './log-sheet';
import { SettingDialog } from './setting-dialog';
import useGraphStore from './store';
import { useAgentHistoryManager } from './use-agent-history-manager';
import { VersionDialog } from './version-dialog';
function AgentDropdownMenuItem({
children,
...props
}: ComponentPropsWithoutRef<typeof DropdownMenuItem>) {
return (
<DropdownMenuItem className="justify-start" {...props}>
{children}
</DropdownMenuItem>
);
}
export default function DataFlow() {
const { navigateToAgents } = useNavigatePage();
const {
visible: chatDrawerVisible,
hideModal: hideChatDrawer,
showModal: showChatDrawer,
} = useSetModalState();
const { t } = useTranslation();
useAgentHistoryManager();
const { handleExportJson } = useHandleExportOrImportJsonFile();
const { saveGraph, loading } = useSaveGraph();
const { flowDetail: agentDetail } = useFetchDataOnMount();
const { handleRun, loading: running } =
useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
const {
visible: versionDialogVisible,
hideModal: hideVersionDialog,
showModal: showVersionDialog,
} = useSetModalState();
const {
visible: settingDialogVisible,
hideModal: hideSettingDialog,
showModal: showSettingDialog,
} = useSetModalState();
const {
visible: logSheetVisible,
showModal: showLogSheet,
hideModal: hideLogSheet,
} = useSetModalState();
const {
isParsing,
logs,
messageId,
setMessageId,
isCompleted,
stopFetchTrace,
isLogEmpty,
} = useFetchLog(logSheetVisible);
const [uploadedFileData, setUploadedFileData] =
useState<Record<string, any>>();
const findNodeByName = useGraphStore((state) => state.findNodeByName);
const handleRunAgent = useCallback(() => {
if (!findNodeByName(Operator.Tokenizer)) {
message.warning(t('dataflow.tokenizerRequired'));
return;
}
if (isParsing) {
// show log sheet
showLogSheet();
} else {
hideLogSheet();
handleRun();
}
}, [findNodeByName, handleRun, hideLogSheet, isParsing, showLogSheet, t]);
const { handleCancel } = useCancelCurrentDataflow({
messageId,
stopFetchTrace,
});
const time = useWatchAgentChange(chatDrawerVisible);
return (
<section className="h-full">
<PageHeader>
<section>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToAgents}>
Agent
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{agentDetail.title}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="text-xs text-text-secondary translate-y-3">
{t('flow.autosaved')} {time}
</div>
</section>
<div className="flex items-center gap-5">
<ButtonLoading
variant={'secondary'}
onClick={() => saveGraph()}
loading={loading}
>
<LaptopMinimalCheck /> {t('flow.save')}
</ButtonLoading>
<ButtonLoading
variant={'secondary'}
onClick={handleRunAgent}
loading={running}
>
<CirclePlay className={isParsing ? 'animate-spin' : ''} />
{isParsing || running ? t('dataflow.running') : t('flow.run')}
</ButtonLoading>
<Button variant={'secondary'} onClick={showVersionDialog}>
<History />
{t('flow.historyversion')}
</Button>
{/* <Button variant={'secondary'}>
<Send />
{t('flow.release')}
</Button> */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'secondary'}>
<ChevronDown /> {t('flow.management')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<AgentDropdownMenuItem onClick={handleExportJson}>
<Upload />
{t('flow.export')}
</AgentDropdownMenuItem>
<DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={showSettingDialog}>
<Settings />
{t('flow.setting')}
</AgentDropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</PageHeader>
<LogContext.Provider
value={{ messageId, setMessageId, setUploadedFileData }}
>
<ReactFlowProvider>
<DropdownProvider>
<DataFlowCanvas
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
showLogSheet={showLogSheet}
></DataFlowCanvas>
</DropdownProvider>
</ReactFlowProvider>
</LogContext.Provider>
{versionDialogVisible && (
<DropdownProvider>
<VersionDialog hideModal={hideVersionDialog}></VersionDialog>
</DropdownProvider>
)}
{settingDialogVisible && (
<SettingDialog hideModal={hideSettingDialog}></SettingDialog>
)}
{logSheetVisible && (
<LogSheet
hideModal={hideLogSheet}
isParsing={isParsing}
isCompleted={isCompleted}
isLogEmpty={isLogEmpty}
logs={logs}
handleCancel={handleCancel}
messageId={messageId}
uploadedFileData={uploadedFileData}
></LogSheet>
)}
</section>
);
}

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