Feat: Add note node #3221 (#8728)

### What problem does this PR solve?

Feat: Add note node #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-07-08 19:18:55 +08:00
committed by GitHub
parent 2a03d49a84
commit 3fe143d84a
17 changed files with 414 additions and 407 deletions

View File

@ -12,41 +12,9 @@ import { RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
import { ResizeIcon, controlStyle } from './resize-icon';
import { ToolBar } from './toolbar';
function ResizeIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="#5025f9"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
style={{
position: 'absolute',
right: 5,
bottom: 5,
}}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="16 20 20 20 20 16" />
<line x1="14" y1="14" x2="20" y2="20" />
<polyline points="8 4 4 4 4 8" />
<line x1="4" y1="4" x2="10" y2="10" />
</svg>
);
}
const controlStyle = {
background: 'transparent',
border: 'none',
cursor: 'nwse-resize',
};
export function InnerIterationNode({
id,
data,

View File

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

View File

@ -0,0 +1,76 @@
import { NodeProps, NodeResizeControl } from '@xyflow/react';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { INoteNode } from '@/interfaces/database/flow';
import { zodResolver } from '@hookform/resolvers/zod';
import { NotebookPen } from 'lucide-react';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { NodeWrapper } from '../node-wrapper';
import { ResizeIcon, controlStyle } from '../resize-icon';
import { useChangeName, useWatchFormChange } from './use-watch-change';
const FormSchema = z.object({
text: z.string(),
});
function NoteNode({ data, id }: NodeProps<INoteNode>) {
const { t } = useTranslation();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: data.form,
});
const { handleChangeName } = useChangeName(id);
useWatchFormChange(id, form);
return (
<NodeWrapper className="p-0 w-full h-full flex flex-col rounded-md ">
<NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}>
<ResizeIcon />
</NodeResizeControl>
<section className="px-1 py-2 flex gap-2 bg-background-highlight items-center note-drag-handle rounded-s-md">
<NotebookPen className="size-4" />
<Input
type="text"
defaultValue={data.name}
onChange={handleChangeName}
></Input>
</section>
<Form {...form}>
<form className="flex-1">
<FormField
control={form.control}
name="text"
render={({ field }) => (
<FormItem className="h-full">
<FormControl>
<Textarea
placeholder={t('flow.notePlaceholder')}
className="resize-none rounded-none p-1 h-full overflow-auto bg-background-header-bar"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</NodeWrapper>
);
}
export default memo(NoteNode);

View File

@ -0,0 +1,31 @@
import useGraphStore from '@/pages/agent/store';
import { useCallback, 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 useChangeName(id: string) {
const updateNodeName = useGraphStore((state) => state.updateNodeName);
const handleChangeName = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
updateNodeName(id, e.target.value.trim());
},
[id, updateNodeName],
);
return { handleChangeName };
}

View File

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