Browse Source

feat: workflow add note node (#5164)

zxhlyh 10 tháng trước cách đây
mục cha
commit
c28d709d7f
69 tập tin đã thay đổi với 2370 bổ sung164 xóa
  1. 5 0
      web/app/components/base/icons/assets/vender/line/editor/bold-01.svg
  2. 5 0
      web/app/components/base/icons/assets/vender/line/editor/dotpoints-01.svg
  3. 3 0
      web/app/components/base/icons/assets/vender/line/editor/italic-01.svg
  4. 5 0
      web/app/components/base/icons/assets/vender/line/editor/strikethrough-01.svg
  5. 8 0
      web/app/components/base/icons/assets/vender/line/editor/title-case.svg
  6. 5 0
      web/app/components/base/icons/assets/vender/line/files/sticker-square.svg
  7. BIN
      web/app/components/base/icons/assets/vender/line/general/Workflow.zip
  8. 5 0
      web/app/components/base/icons/assets/vender/line/general/link-01.svg
  9. 10 0
      web/app/components/base/icons/assets/vender/line/general/link-broken-01.svg
  10. 39 0
      web/app/components/base/icons/src/vender/line/editor/Bold01.json
  11. 16 0
      web/app/components/base/icons/src/vender/line/editor/Bold01.tsx
  12. 39 0
      web/app/components/base/icons/src/vender/line/editor/Dotpoints01.json
  13. 16 0
      web/app/components/base/icons/src/vender/line/editor/Dotpoints01.tsx
  14. 29 0
      web/app/components/base/icons/src/vender/line/editor/Italic01.json
  15. 16 0
      web/app/components/base/icons/src/vender/line/editor/Italic01.tsx
  16. 39 0
      web/app/components/base/icons/src/vender/line/editor/Strikethrough01.json
  17. 16 0
      web/app/components/base/icons/src/vender/line/editor/Strikethrough01.tsx
  18. 53 0
      web/app/components/base/icons/src/vender/line/editor/TitleCase.json
  19. 16 0
      web/app/components/base/icons/src/vender/line/editor/TitleCase.tsx
  20. 5 0
      web/app/components/base/icons/src/vender/line/editor/index.ts
  21. 39 0
      web/app/components/base/icons/src/vender/line/files/StickerSquare.json
  22. 16 0
      web/app/components/base/icons/src/vender/line/files/StickerSquare.tsx
  23. 1 0
      web/app/components/base/icons/src/vender/line/files/index.ts
  24. 39 0
      web/app/components/base/icons/src/vender/line/general/Link01.json
  25. 16 0
      web/app/components/base/icons/src/vender/line/general/Link01.tsx
  26. 66 0
      web/app/components/base/icons/src/vender/line/general/LinkBroken01.json
  27. 16 0
      web/app/components/base/icons/src/vender/line/general/LinkBroken01.tsx
  28. 2 0
      web/app/components/base/icons/src/vender/line/general/index.ts
  29. 18 1
      web/app/components/workflow/candidate-node.tsx
  30. 1 0
      web/app/components/workflow/constants.ts
  31. 21 15
      web/app/components/workflow/hooks/use-checklist.ts
  32. 2 1
      web/app/components/workflow/hooks/use-node-data-update.ts
  33. 19 2
      web/app/components/workflow/hooks/use-nodes-interactions.ts
  34. 7 2
      web/app/components/workflow/hooks/use-workflow.ts
  35. 6 2
      web/app/components/workflow/index.tsx
  36. 12 3
      web/app/components/workflow/nodes/_base/components/node-resizer.tsx
  37. 2 0
      web/app/components/workflow/nodes/constants.ts
  38. 21 7
      web/app/components/workflow/nodes/index.tsx
  39. 42 0
      web/app/components/workflow/note-node/constants.ts
  40. 29 0
      web/app/components/workflow/note-node/hooks.ts
  41. 127 0
      web/app/components/workflow/note-node/index.tsx
  42. 65 0
      web/app/components/workflow/note-node/note-editor/context.tsx
  43. 62 0
      web/app/components/workflow/note-node/note-editor/editor.tsx
  44. 3 0
      web/app/components/workflow/note-node/note-editor/index.tsx
  45. 78 0
      web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/hooks.ts
  46. 9 0
      web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/index.tsx
  47. 152 0
      web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx
  48. 115 0
      web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts
  49. 25 0
      web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/index.tsx
  50. 72 0
      web/app/components/workflow/note-node/note-editor/store.ts
  51. 17 0
      web/app/components/workflow/note-node/note-editor/theme/index.ts
  52. 24 0
      web/app/components/workflow/note-node/note-editor/theme/theme.css
  53. 105 0
      web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx
  54. 81 0
      web/app/components/workflow/note-node/note-editor/toolbar/command.tsx
  55. 7 0
      web/app/components/workflow/note-node/note-editor/toolbar/divider.tsx
  56. 86 0
      web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx
  57. 147 0
      web/app/components/workflow/note-node/note-editor/toolbar/hooks.ts
  58. 48 0
      web/app/components/workflow/note-node/note-editor/toolbar/index.tsx
  59. 107 0
      web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx
  60. 21 0
      web/app/components/workflow/note-node/note-editor/utils.ts
  61. 17 0
      web/app/components/workflow/note-node/types.ts
  62. 27 1
      web/app/components/workflow/operator/control.tsx
  63. 41 0
      web/app/components/workflow/operator/hooks.ts
  64. 12 0
      web/app/components/workflow/panel-contextmenu.tsx
  65. 6 4
      web/app/components/workflow/utils.ts
  66. 19 0
      web/i18n/en-US/workflow.ts
  67. 19 0
      web/i18n/zh-Hans/workflow.ts
  68. 2 2
      web/package.json
  69. 171 124
      web/yarn.lock

+ 5 - 0
web/app/components/base/icons/assets/vender/line/editor/bold-01.svg

@@ -0,0 +1,5 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon">
+<path id="Icon_2" d="M4.5 7.99996H9.83333C11.3061 7.99996 12.5 6.80605 12.5 5.33329C12.5 3.86053 11.3061 2.66663 9.83333 2.66663H4.5V7.99996ZM4.5 7.99996H10.5C11.9728 7.99996 13.1667 9.19387 13.1667 10.6666C13.1667 12.1394 11.9728 13.3333 10.5 13.3333H4.5V7.99996Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/line/editor/dotpoints-01.svg

@@ -0,0 +1,5 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon">
+<path id="Icon_2" d="M14.5 8.00004L6.5 8.00004M14.5 4.00004L6.5 4.00004M14.5 12L6.5 12M3.83333 8.00004C3.83333 8.36823 3.53486 8.66671 3.16667 8.66671C2.79848 8.66671 2.5 8.36823 2.5 8.00004C2.5 7.63185 2.79848 7.33337 3.16667 7.33337C3.53486 7.33337 3.83333 7.63185 3.83333 8.00004ZM3.83333 4.00004C3.83333 4.36823 3.53486 4.66671 3.16667 4.66671C2.79848 4.66671 2.5 4.36823 2.5 4.00004C2.5 3.63185 2.79848 3.33337 3.16667 3.33337C3.53486 3.33337 3.83333 3.63185 3.83333 4.00004ZM3.83333 12C3.83333 12.3682 3.53486 12.6667 3.16667 12.6667C2.79848 12.6667 2.5 12.3682 2.5 12C2.5 11.6319 2.79848 11.3334 3.16667 11.3334C3.53486 11.3334 3.83333 11.6319 3.83333 12Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 3 - 0
web/app/components/base/icons/assets/vender/line/editor/italic-01.svg

@@ -0,0 +1,3 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.1666 2.66663H7.16659M9.83325 13.3333H3.83325M10.4999 2.66663L6.49992 13.3333" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/line/editor/strikethrough-01.svg

@@ -0,0 +1,5 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon">
+<path id="Icon_2" d="M4.5 10.6666C4.5 12.1394 5.69391 13.3333 7.16667 13.3333H9.83333C11.3061 13.3333 12.5 12.1394 12.5 10.6666C12.5 9.19387 11.3061 7.99996 9.83333 7.99996M12.5 5.33329C12.5 3.86053 11.3061 2.66663 9.83333 2.66663H7.16667C5.69391 2.66663 4.5 3.86053 4.5 5.33329M2.5 7.99996H14.5" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 8 - 0
web/app/components/base/icons/assets/vender/line/editor/title-case.svg

@@ -0,0 +1,8 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon">
+<g id="Vector">
+<path d="M2.0922 12.4865C2.57616 12.4865 2.84839 12.2445 3.01778 11.6638L3.47754 10.3026H6.62933L7.0891 11.6819C7.25243 12.2506 7.52463 12.4865 8.03887 12.4865C8.5712 12.4865 8.95232 12.1295 8.95232 11.6275C8.95232 11.4459 8.92208 11.2827 8.83743 11.0467L6.44179 4.54954C6.18167 3.83569 5.7582 3.52112 5.04436 3.52112C4.35471 3.52112 3.9252 3.84779 3.67112 4.55559L1.28762 11.0467C1.20897 11.2705 1.16663 11.4762 1.16663 11.6275C1.16663 12.1538 1.52355 12.4865 2.0922 12.4865ZM3.8768 8.88703L5.00806 5.31177H5.05041L6.20586 8.88703H3.8768Z" fill="#344054"/>
+<path d="M12.1068 12.4744C12.9174 12.4744 13.7281 12.0691 14.091 11.3795H14.1273V11.7122C14.1636 12.2324 14.4963 12.4986 14.9742 12.4986C15.4764 12.4986 15.8333 12.1961 15.8333 11.6093V7.91309C15.8333 6.60636 14.7504 5.74734 13.0868 5.74734C11.7438 5.74734 10.7033 6.22525 10.4008 6.99957C10.3403 7.13269 10.3101 7.25973 10.3101 7.39885C10.3101 7.79813 10.6186 8.07638 11.0481 8.07638C11.3324 8.07638 11.5563 7.9675 11.7499 7.74973C12.1431 7.24157 12.4697 7.06613 13.0081 7.06613C13.6736 7.06613 14.0971 7.41701 14.0971 8.02198V8.4515L12.4637 8.54823C10.8424 8.64503 9.93506 9.32864 9.93506 10.5083C9.93506 11.6759 10.8727 12.4744 12.1068 12.4744ZM12.6876 11.1979C12.0947 11.1979 11.6954 10.8955 11.6954 10.4115C11.6954 9.95176 12.0705 9.65528 12.7299 9.60695L14.0971 9.52224V9.99408C14.0971 10.6958 13.4619 11.1979 12.6876 11.1979Z" fill="#344054"/>
+</g>
+</g>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/line/files/sticker-square.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="sticker-square">
+<path id="Icon" d="M8.66667 2.33333V4.13333C8.66667 5.25344 8.66667 5.81349 8.88465 6.24131C9.0764 6.61764 9.38236 6.9236 9.75869 7.11535C10.1865 7.33333 10.7466 7.33333 11.8667 7.33333H13.6667M14 8.65882V10.8C14 11.9201 14 12.4802 13.782 12.908C13.5903 13.2843 13.2843 13.5903 12.908 13.782C12.4802 14 11.9201 14 10.8 14H5.2C4.0799 14 3.51984 14 3.09202 13.782C2.71569 13.5903 2.40973 13.2843 2.21799 12.908C2 12.4802 2 11.9201 2 10.8V5.2C2 4.0799 2 3.51984 2.21799 3.09202C2.40973 2.71569 2.71569 2.40973 3.09202 2.21799C3.51984 2 4.0799 2 5.2 2H7.34118C7.83036 2 8.07496 2 8.30513 2.05526C8.5092 2.10425 8.70429 2.18506 8.88324 2.29472C9.08507 2.4184 9.25802 2.59135 9.60393 2.93726L13.0627 6.39608C13.4086 6.74198 13.5816 6.91493 13.7053 7.11676C13.8149 7.29571 13.8957 7.4908 13.9447 7.69487C14 7.92505 14 8.16964 14 8.65882Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

BIN
web/app/components/base/icons/assets/vender/line/general/Workflow.zip


+ 5 - 0
web/app/components/base/icons/assets/vender/line/general/link-01.svg

@@ -0,0 +1,5 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon">
+<path id="Icon_2" d="M8.97167 12.2427L8.02886 13.1855C6.72711 14.4872 4.61656 14.4872 3.31481 13.1855C2.01306 11.8837 2.01306 9.77317 3.31481 8.47142L4.25762 7.52861M12.7429 8.47142L13.6857 7.52861C14.9875 6.22687 14.9875 4.11632 13.6857 2.81457C12.384 1.51282 10.2734 1.51282 8.97167 2.81457L8.02886 3.75738M6.16693 10.3333L10.8336 5.66667" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 10 - 0
web/app/components/base/icons/assets/vender/line/general/link-broken-01.svg

@@ -0,0 +1,10 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Left Icon" clip-path="url(#clip0_6246_47371)">
+<path id="Icon" d="M4.5 2V1M7.5 10V11M2 4.5H1M10 7.5H11M2.45711 2.45711L1.75 1.75M9.54289 9.54289L10.25 10.25M6 8.82843L4.93934 9.88909C4.15829 10.6701 2.89196 10.6701 2.11091 9.88909C1.32986 9.10804 1.32986 7.84171 2.11091 7.06066L3.17157 6M8.82843 6L9.88909 4.93934C10.6701 4.15829 10.6701 2.89196 9.88909 2.11091C9.10804 1.32986 7.84171 1.32986 7.06066 2.11091L6 3.17157" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_6246_47371">
+<rect width="12" height="12" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 39 - 0
web/app/components/base/icons/src/vender/line/editor/Bold01.json

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "17",
+			"height": "16",
+			"viewBox": "0 0 17 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Icon"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon_2",
+							"d": "M4.5 7.99996H9.83333C11.3061 7.99996 12.5 6.80605 12.5 5.33329C12.5 3.86053 11.3061 2.66663 9.83333 2.66663H4.5V7.99996ZM4.5 7.99996H10.5C11.9728 7.99996 13.1667 9.19387 13.1667 10.6666C13.1667 12.1394 11.9728 13.3333 10.5 13.3333H4.5V7.99996Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Bold01"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/editor/Bold01.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Bold01.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Bold01'
+
+export default Icon

+ 39 - 0
web/app/components/base/icons/src/vender/line/editor/Dotpoints01.json

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "17",
+			"height": "16",
+			"viewBox": "0 0 17 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Icon"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon_2",
+							"d": "M14.5 8.00004L6.5 8.00004M14.5 4.00004L6.5 4.00004M14.5 12L6.5 12M3.83333 8.00004C3.83333 8.36823 3.53486 8.66671 3.16667 8.66671C2.79848 8.66671 2.5 8.36823 2.5 8.00004C2.5 7.63185 2.79848 7.33337 3.16667 7.33337C3.53486 7.33337 3.83333 7.63185 3.83333 8.00004ZM3.83333 4.00004C3.83333 4.36823 3.53486 4.66671 3.16667 4.66671C2.79848 4.66671 2.5 4.36823 2.5 4.00004C2.5 3.63185 2.79848 3.33337 3.16667 3.33337C3.53486 3.33337 3.83333 3.63185 3.83333 4.00004ZM3.83333 12C3.83333 12.3682 3.53486 12.6667 3.16667 12.6667C2.79848 12.6667 2.5 12.3682 2.5 12C2.5 11.6319 2.79848 11.3334 3.16667 11.3334C3.53486 11.3334 3.83333 11.6319 3.83333 12Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Dotpoints01"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/editor/Dotpoints01.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Dotpoints01.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Dotpoints01'
+
+export default Icon

+ 29 - 0
web/app/components/base/icons/src/vender/line/editor/Italic01.json

@@ -0,0 +1,29 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "17",
+			"height": "16",
+			"viewBox": "0 0 17 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M13.1666 2.66663H7.16659M9.83325 13.3333H3.83325M10.4999 2.66663L6.49992 13.3333",
+					"stroke": "currentColor",
+					"stroke-width": "1.5",
+					"stroke-linecap": "round",
+					"stroke-linejoin": "round"
+				},
+				"children": []
+			}
+		]
+	},
+	"name": "Italic01"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/editor/Italic01.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Italic01.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Italic01'
+
+export default Icon

+ 39 - 0
web/app/components/base/icons/src/vender/line/editor/Strikethrough01.json

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "17",
+			"height": "16",
+			"viewBox": "0 0 17 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Icon"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon_2",
+							"d": "M4.5 10.6666C4.5 12.1394 5.69391 13.3333 7.16667 13.3333H9.83333C11.3061 13.3333 12.5 12.1394 12.5 10.6666C12.5 9.19387 11.3061 7.99996 9.83333 7.99996M12.5 5.33329C12.5 3.86053 11.3061 2.66663 9.83333 2.66663H7.16667C5.69391 2.66663 4.5 3.86053 4.5 5.33329M2.5 7.99996H14.5",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Strikethrough01"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/editor/Strikethrough01.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Strikethrough01.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Strikethrough01'
+
+export default Icon

+ 53 - 0
web/app/components/base/icons/src/vender/line/editor/TitleCase.json

@@ -0,0 +1,53 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "17",
+			"height": "16",
+			"viewBox": "0 0 17 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Icon"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Vector"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M2.0922 12.4865C2.57616 12.4865 2.84839 12.2445 3.01778 11.6638L3.47754 10.3026H6.62933L7.0891 11.6819C7.25243 12.2506 7.52463 12.4865 8.03887 12.4865C8.5712 12.4865 8.95232 12.1295 8.95232 11.6275C8.95232 11.4459 8.92208 11.2827 8.83743 11.0467L6.44179 4.54954C6.18167 3.83569 5.7582 3.52112 5.04436 3.52112C4.35471 3.52112 3.9252 3.84779 3.67112 4.55559L1.28762 11.0467C1.20897 11.2705 1.16663 11.4762 1.16663 11.6275C1.16663 12.1538 1.52355 12.4865 2.0922 12.4865ZM3.8768 8.88703L5.00806 5.31177H5.05041L6.20586 8.88703H3.8768Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M12.1068 12.4744C12.9174 12.4744 13.7281 12.0691 14.091 11.3795H14.1273V11.7122C14.1636 12.2324 14.4963 12.4986 14.9742 12.4986C15.4764 12.4986 15.8333 12.1961 15.8333 11.6093V7.91309C15.8333 6.60636 14.7504 5.74734 13.0868 5.74734C11.7438 5.74734 10.7033 6.22525 10.4008 6.99957C10.3403 7.13269 10.3101 7.25973 10.3101 7.39885C10.3101 7.79813 10.6186 8.07638 11.0481 8.07638C11.3324 8.07638 11.5563 7.9675 11.7499 7.74973C12.1431 7.24157 12.4697 7.06613 13.0081 7.06613C13.6736 7.06613 14.0971 7.41701 14.0971 8.02198V8.4515L12.4637 8.54823C10.8424 8.64503 9.93506 9.32864 9.93506 10.5083C9.93506 11.6759 10.8727 12.4744 12.1068 12.4744ZM12.6876 11.1979C12.0947 11.1979 11.6954 10.8955 11.6954 10.4115C11.6954 9.95176 12.0705 9.65528 12.7299 9.60695L14.0971 9.52224V9.99408C14.0971 10.6958 13.4619 11.1979 12.6876 11.1979Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "TitleCase"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/editor/TitleCase.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './TitleCase.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'TitleCase'
+
+export default Icon

+ 5 - 0
web/app/components/base/icons/src/vender/line/editor/index.ts

@@ -1,11 +1,16 @@
 export { default as AlignLeft } from './AlignLeft'
 export { default as BezierCurve03 } from './BezierCurve03'
+export { default as Bold01 } from './Bold01'
 export { default as Colors } from './Colors'
 export { default as Cursor02C } from './Cursor02C'
+export { default as Dotpoints01 } from './Dotpoints01'
 export { default as Hand02 } from './Hand02'
 export { default as ImageIndentLeft } from './ImageIndentLeft'
+export { default as Italic01 } from './Italic01'
 export { default as LeftIndent02 } from './LeftIndent02'
 export { default as LetterSpacing01 } from './LetterSpacing01'
+export { default as Strikethrough01 } from './Strikethrough01'
+export { default as TitleCase } from './TitleCase'
 export { default as TypeSquare } from './TypeSquare'
 export { default as ZoomIn } from './ZoomIn'
 export { default as ZoomOut } from './ZoomOut'

+ 39 - 0
web/app/components/base/icons/src/vender/line/files/StickerSquare.json

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "sticker-square"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M8.66667 2.33333V4.13333C8.66667 5.25344 8.66667 5.81349 8.88465 6.24131C9.0764 6.61764 9.38236 6.9236 9.75869 7.11535C10.1865 7.33333 10.7466 7.33333 11.8667 7.33333H13.6667M14 8.65882V10.8C14 11.9201 14 12.4802 13.782 12.908C13.5903 13.2843 13.2843 13.5903 12.908 13.782C12.4802 14 11.9201 14 10.8 14H5.2C4.0799 14 3.51984 14 3.09202 13.782C2.71569 13.5903 2.40973 13.2843 2.21799 12.908C2 12.4802 2 11.9201 2 10.8V5.2C2 4.0799 2 3.51984 2.21799 3.09202C2.40973 2.71569 2.71569 2.40973 3.09202 2.21799C3.51984 2 4.0799 2 5.2 2H7.34118C7.83036 2 8.07496 2 8.30513 2.05526C8.5092 2.10425 8.70429 2.18506 8.88324 2.29472C9.08507 2.4184 9.25802 2.59135 9.60393 2.93726L13.0627 6.39608C13.4086 6.74198 13.5816 6.91493 13.7053 7.11676C13.8149 7.29571 13.8957 7.4908 13.9447 7.69487C14 7.92505 14 8.16964 14 8.65882Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "StickerSquare"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/files/StickerSquare.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './StickerSquare.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'StickerSquare'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/line/files/index.ts

@@ -9,3 +9,4 @@ export { default as FilePlus02 } from './FilePlus02'
 export { default as FileText } from './FileText'
 export { default as FileUpload } from './FileUpload'
 export { default as Folder } from './Folder'
+export { default as StickerSquare } from './StickerSquare'

+ 39 - 0
web/app/components/base/icons/src/vender/line/general/Link01.json

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "17",
+			"height": "16",
+			"viewBox": "0 0 17 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Icon"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon_2",
+							"d": "M8.97167 12.2427L8.02886 13.1855C6.72711 14.4872 4.61656 14.4872 3.31481 13.1855C2.01306 11.8837 2.01306 9.77317 3.31481 8.47142L4.25762 7.52861M12.7429 8.47142L13.6857 7.52861C14.9875 6.22687 14.9875 4.11632 13.6857 2.81457C12.384 1.51282 10.2734 1.51282 8.97167 2.81457L8.02886 3.75738M6.16693 10.3333L10.8336 5.66667",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Link01"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/general/Link01.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Link01.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Link01'
+
+export default Icon

+ 66 - 0
web/app/components/base/icons/src/vender/line/general/LinkBroken01.json

@@ -0,0 +1,66 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "12",
+			"height": "12",
+			"viewBox": "0 0 12 12",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Left Icon",
+					"clip-path": "url(#clip0_6246_47371)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M4.5 2V1M7.5 10V11M2 4.5H1M10 7.5H11M2.45711 2.45711L1.75 1.75M9.54289 9.54289L10.25 10.25M6 8.82843L4.93934 9.88909C4.15829 10.6701 2.89196 10.6701 2.11091 9.88909C1.32986 9.10804 1.32986 7.84171 2.11091 7.06066L3.17157 6M8.82843 6L9.88909 4.93934C10.6701 4.15829 10.6701 2.89196 9.88909 2.11091C9.10804 1.32986 7.84171 1.32986 7.06066 2.11091L6 3.17157",
+							"stroke": "currentColor",
+							"stroke-width": "1.25",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_6246_47371"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "12",
+									"height": "12",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "LinkBroken01"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/general/LinkBroken01.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './LinkBroken01.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'LinkBroken01'
+
+export default Icon

+ 2 - 0
web/app/components/base/icons/src/vender/line/general/index.ts

@@ -14,7 +14,9 @@ export { default as Edit05 } from './Edit05'
 export { default as Hash02 } from './Hash02'
 export { default as HelpCircle } from './HelpCircle'
 export { default as InfoCircle } from './InfoCircle'
+export { default as Link01 } from './Link01'
 export { default as Link03 } from './Link03'
+export { default as LinkBroken01 } from './LinkBroken01'
 export { default as LinkExternal01 } from './LinkExternal01'
 export { default as LinkExternal02 } from './LinkExternal02'
 export { default as Loading02 } from './Loading02'

+ 18 - 1
web/app/components/workflow/candidate-node.tsx

@@ -12,7 +12,11 @@ import {
   useStore,
   useWorkflowStore,
 } from './store'
+import { useNodesInteractions } from './hooks'
+import { CUSTOM_NODE } from './constants'
 import CustomNode from './nodes'
+import CustomNoteNode from './note-node'
+import { CUSTOM_NOTE_NODE } from './note-node/constants'
 
 const CandidateNode = () => {
   const store = useStoreApi()
@@ -21,6 +25,7 @@ const CandidateNode = () => {
   const candidateNode = useStore(s => s.candidateNode)
   const mousePosition = useStore(s => s.mousePosition)
   const { zoom } = useViewport()
+  const { handleNodeSelect } = useNodesInteractions()
 
   useEventListener('click', (e) => {
     const { candidateNode, mousePosition } = workflowStore.getState()
@@ -49,6 +54,9 @@ const CandidateNode = () => {
       })
       setNodes(newNodes)
       workflowStore.setState({ candidateNode: undefined })
+
+      if (candidateNode.type === CUSTOM_NOTE_NODE)
+        handleNodeSelect(candidateNode.id)
     }
   })
 
@@ -73,7 +81,16 @@ const CandidateNode = () => {
         transformOrigin: '0 0',
       }}
     >
-      <CustomNode {...candidateNode as any} />
+      {
+        candidateNode.type === CUSTOM_NODE && (
+          <CustomNode {...candidateNode as any} />
+        )
+      }
+      {
+        candidateNode.type === CUSTOM_NOTE_NODE && (
+          <CustomNoteNode {...candidateNode as any} />
+        )
+      }
     </div>
   )
 }

+ 1 - 0
web/app/components/workflow/constants.ts

@@ -391,3 +391,4 @@ export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [
 ]
 
 export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE'
+export const CUSTOM_NODE = 'custom'

+ 21 - 15
web/app/components/workflow/hooks/use-checklist.ts

@@ -14,7 +14,10 @@ import {
   getToolCheckParams,
   getValidTreeNodes,
 } from '../utils'
-import { MAX_TREE_DEEPTH } from '../constants'
+import {
+  CUSTOM_NODE,
+  MAX_TREE_DEEPTH,
+} from '../constants'
 import type { ToolNodeType } from '../nodes/tool/types'
 import { useIsChatMode } from './use-workflow'
 import { useNodesExtraData } from './use-nodes-data'
@@ -33,7 +36,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
 
   const needWarningNodes = useMemo(() => {
     const list = []
-    const { validNodes } = getValidTreeNodes(nodes, edges)
+    const { validNodes } = getValidTreeNodes(nodes.filter(node => node.type === CUSTOM_NODE), edges)
 
     for (let i = 0; i < nodes.length; i++) {
       const node = nodes[i]
@@ -53,17 +56,20 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
         if (provider_type === CollectionType.workflow)
           toolIcon = workflowTools.find(tool => tool.id === node.data.provider_id)?.icon
       }
-      const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid)
-
-      if (errorMessage || !validNodes.find(n => n.id === node.id)) {
-        list.push({
-          id: node.id,
-          type: node.data.type,
-          title: node.data.title,
-          toolIcon,
-          unConnected: !validNodes.find(n => n.id === node.id),
-          errorMessage,
-        })
+
+      if (node.type === CUSTOM_NODE) {
+        const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid)
+
+        if (errorMessage || !validNodes.find(n => n.id === node.id)) {
+          list.push({
+            id: node.id,
+            type: node.data.type,
+            title: node.data.title,
+            toolIcon,
+            unConnected: !validNodes.find(n => n.id === node.id),
+            errorMessage,
+          })
+        }
       }
     }
 
@@ -107,11 +113,11 @@ export const useChecklistBeforePublish = () => {
       getNodes,
       edges,
     } = store.getState()
-    const nodes = getNodes()
+    const nodes = getNodes().filter(node => node.type === CUSTOM_NODE)
     const {
       validNodes,
       maxDepth,
-    } = getValidTreeNodes(nodes, edges)
+    } = getValidTreeNodes(nodes.filter(node => node.type === CUSTOM_NODE), edges)
 
     if (maxDepth > MAX_TREE_DEEPTH) {
       notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEEPTH }) })

+ 2 - 1
web/app/components/workflow/hooks/use-node-data-update.ts

@@ -22,7 +22,8 @@ export const useNodeDataUpdate = () => {
     const newNodes = produce(getNodes(), (draft) => {
       const currentNode = draft.find(node => node.id === id)!
 
-      currentNode.data = { ...currentNode?.data, ...data }
+      if (currentNode)
+        currentNode.data = { ...currentNode.data, ...data }
     })
     setNodes(newNodes)
   }, [store])

+ 19 - 2
web/app/components/workflow/hooks/use-nodes-interactions.ts

@@ -38,6 +38,7 @@ import {
   getNodesConnectedSourceOrTargetHandleIdsMap,
   getTopLeftNodePosition,
 } from '../utils'
+import { CUSTOM_NOTE_NODE } from '../note-node/constants'
 import type { IterationNodeType } from '../nodes/iteration/types'
 import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
 import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
@@ -71,7 +72,7 @@ export const useNodesInteractions = () => {
     if (getNodesReadOnly())
       return
 
-    if (node.data.isIterationStart)
+    if (node.data.isIterationStart || node.type === CUSTOM_NOTE_NODE)
       return
 
     dragNodeStartPosition.current = { x: node.position.x, y: node.position.y }
@@ -143,6 +144,9 @@ export const useNodesInteractions = () => {
     if (getNodesReadOnly())
       return
 
+    if (node.type === CUSTOM_NOTE_NODE)
+      return
+
     const {
       getNodes,
       setNodes,
@@ -193,10 +197,13 @@ export const useNodesInteractions = () => {
     setEdges(newEdges)
   }, [store, workflowStore, getNodesReadOnly])
 
-  const handleNodeLeave = useCallback<NodeMouseHandler>(() => {
+  const handleNodeLeave = useCallback<NodeMouseHandler>((_, node) => {
     if (getNodesReadOnly())
       return
 
+    if (node.type === CUSTOM_NOTE_NODE)
+      return
+
     const {
       setEnteringNodePayload,
     } = workflowStore.getState()
@@ -298,6 +305,9 @@ export const useNodesInteractions = () => {
     if (targetNode?.data.isIterationStart)
       return
 
+    if (sourceNode?.type === CUSTOM_NOTE_NODE || targetNode?.type === CUSTOM_NOTE_NODE)
+      return
+
     const needDeleteEdges = edges.filter((edge) => {
       if (
         (edge.source === source && edge.sourceHandle === sourceHandle)
@@ -361,6 +371,9 @@ export const useNodesInteractions = () => {
       const { getNodes } = store.getState()
       const node = getNodes().find(n => n.id === nodeId)!
 
+      if (node.type === CUSTOM_NOTE_NODE)
+        return
+
       if (node.data.type === BlockEnum.VariableAggregator || node.data.type === BlockEnum.VariableAssigner) {
         if (handleType === 'target')
           return
@@ -975,6 +988,9 @@ export const useNodesInteractions = () => {
   }, [store])
 
   const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => {
+    if (node.type === CUSTOM_NOTE_NODE)
+      return
+
     e.preventDefault()
     const container = document.querySelector('#workflow-container')
     const { x, y } = container!.getBoundingClientRect()
@@ -1051,6 +1067,7 @@ export const useNodesInteractions = () => {
         const nodeType = nodeToPaste.data.type
 
         const newNode = generateNewNode({
+          type: nodeToPaste.type,
           data: {
             ...NODES_INITIAL_DATA[nodeType],
             ...nodeToPaste.data,

+ 7 - 2
web/app/components/workflow/hooks/use-workflow.ts

@@ -34,8 +34,10 @@ import {
   useWorkflowStore,
 } from '../store'
 import {
+  CUSTOM_NODE,
   SUPPORT_OUTPUT_VARS_NODE,
 } from '../constants'
+import { CUSTOM_NOTE_NODE } from '../note-node/constants'
 import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
 import { useNodesExtraData } from './use-nodes-data'
 import { useWorkflowTemplate } from './use-workflow-template'
@@ -88,7 +90,7 @@ export const useWorkflow = () => {
     const rankMap = {} as Record<string, Node>
 
     nodes.forEach((node) => {
-      if (!node.parentId) {
+      if (!node.parentId && node.type === CUSTOM_NODE) {
         const rank = layout.node(node.id).rank!
 
         if (!rankMap[rank]) {
@@ -103,7 +105,7 @@ export const useWorkflow = () => {
 
     const newNodes = produce(nodes, (draft) => {
       draft.forEach((node) => {
-        if (!node.parentId) {
+        if (!node.parentId && node.type === CUSTOM_NODE) {
           const nodeWithPosition = layout.node(node.id)
 
           node.position = {
@@ -345,6 +347,9 @@ export const useWorkflow = () => {
     if (targetNode.data.isIterationStart)
       return false
 
+    if (sourceNode.type === CUSTOM_NOTE_NODE || targetNode.type === CUSTOM_NOTE_NODE)
+      return false
+
     if (sourceNode && targetNode) {
       const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes
       const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start]

+ 6 - 2
web/app/components/workflow/index.tsx

@@ -46,6 +46,8 @@ import {
 } from './hooks'
 import Header from './header'
 import CustomNode from './nodes'
+import CustomNoteNode from './note-node'
+import { CUSTOM_NOTE_NODE } from './note-node/constants'
 import Operator from './operator'
 import CustomEdge from './custom-edge'
 import CustomConnectionLine from './custom-connection-line'
@@ -66,6 +68,7 @@ import {
   initialNodes,
 } from './utils'
 import {
+  CUSTOM_NODE,
   ITERATION_CHILDREN_Z_INDEX,
   WORKFLOW_DATA_UPDATE,
 } from './constants'
@@ -76,10 +79,11 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
 import Confirm from '@/app/components/base/confirm/common'
 
 const nodeTypes = {
-  custom: CustomNode,
+  [CUSTOM_NODE]: CustomNode,
+  [CUSTOM_NOTE_NODE]: CustomNoteNode,
 }
 const edgeTypes = {
-  custom: CustomEdge,
+  [CUSTOM_NODE]: CustomEdge,
 }
 
 type WorkflowProps = {

+ 12 - 3
web/app/components/workflow/nodes/_base/components/node-resizer.tsx

@@ -19,10 +19,18 @@ const Icon = () => {
 type NodeResizerProps = {
   nodeId: string
   nodeData: CommonNodeType
+  icon?: JSX.Element
+  minWidth?: number
+  minHeight?: number
+  maxWidth?: number
 }
 const NodeResizer = ({
   nodeId,
   nodeData,
+  icon = <Icon />,
+  minWidth = 272,
+  minHeight = 176,
+  maxWidth,
 }: NodeResizerProps) => {
   const { handleNodeResize } = useNodesInteractions()
 
@@ -39,10 +47,11 @@ const NodeResizer = ({
         position='bottom-right'
         className='!border-none !bg-transparent'
         onResize={handleResize}
-        minWidth={272}
-        minHeight={176}
+        minWidth={minWidth}
+        minHeight={minHeight}
+        maxWidth={maxWidth}
       >
-        <div className='absolute bottom-[1px] right-[1px]'><Icon /></div>
+        <div className='absolute bottom-[1px] right-[1px]'>{icon}</div>
       </NodeResizeControl>
     </div>
   )

+ 2 - 0
web/app/components/workflow/nodes/constants.ts

@@ -64,3 +64,5 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
   [BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
   [BlockEnum.Iteration]: IterationPanel,
 }
+
+export const CUSTOM_NODE_TYPE = 'custom'

+ 21 - 7
web/app/components/workflow/nodes/index.tsx

@@ -1,6 +1,10 @@
-import { memo } from 'react'
+import {
+  memo,
+  useMemo,
+} from 'react'
 import type { NodeProps } from 'reactflow'
 import type { Node } from '../types'
+import { CUSTOM_NODE } from '../constants'
 import {
   NodeComponentMap,
   PanelComponentMap,
@@ -23,14 +27,24 @@ const CustomNode = (props: NodeProps) => {
 CustomNode.displayName = 'CustomNode'
 
 export const Panel = memo((props: Node) => {
+  const nodeClass = props.type
   const nodeData = props.data
-  const PanelComponent = PanelComponentMap[nodeData.type]
+  const PanelComponent = useMemo(() => {
+    if (nodeClass === CUSTOM_NODE)
+      return PanelComponentMap[nodeData.type]
 
-  return (
-    <BasePanel key={props.id} {...props}>
-      <PanelComponent />
-    </BasePanel>
-  )
+    return () => null
+  }, [nodeClass, nodeData.type])
+
+  if (nodeClass === CUSTOM_NODE) {
+    return (
+      <BasePanel key={props.id} {...props}>
+        <PanelComponent />
+      </BasePanel>
+    )
+  }
+
+  return null
 })
 
 Panel.displayName = 'Panel'

+ 42 - 0
web/app/components/workflow/note-node/constants.ts

@@ -0,0 +1,42 @@
+import { NoteTheme } from './types'
+
+export const CUSTOM_NOTE_NODE = 'custom-note'
+
+export const THEME_MAP: Record<string, { outer: string; title: string; bg: string; border: string }> = {
+  [NoteTheme.blue]: {
+    outer: '#2E90FA',
+    title: '#D1E9FF',
+    bg: '#EFF8FF',
+    border: '#84CAFF',
+  },
+  [NoteTheme.cyan]: {
+    outer: '#06AED4',
+    title: '#CFF9FE',
+    bg: '#ECFDFF',
+    border: '#67E3F9',
+  },
+  [NoteTheme.green]: {
+    outer: '#16B364',
+    title: '#D3F8DF',
+    bg: '#EDFCF2',
+    border: '#73E2A3',
+  },
+  [NoteTheme.yellow]: {
+    outer: '#EAAA08',
+    title: '#FEF7C3',
+    bg: '#FEFBE8',
+    border: '#FDE272',
+  },
+  [NoteTheme.pink]: {
+    outer: '#EE46BC',
+    title: '#FCE7F6',
+    bg: '#FDF2FA',
+    border: '#FAA7E0',
+  },
+  [NoteTheme.violet]: {
+    outer: '#875BF7',
+    title: '#ECE9FE',
+    bg: '#F5F3FF',
+    border: '#C3B5FD',
+  },
+}

+ 29 - 0
web/app/components/workflow/note-node/hooks.ts

@@ -0,0 +1,29 @@
+import { useCallback } from 'react'
+import type { EditorState } from 'lexical'
+import { useNodeDataUpdate } from '../hooks'
+import type { NoteTheme } from './types'
+
+export const useNote = (id: string) => {
+  const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
+
+  const handleThemeChange = useCallback((theme: NoteTheme) => {
+    handleNodeDataUpdateWithSyncDraft({ id, data: { theme } })
+  }, [handleNodeDataUpdateWithSyncDraft, id])
+
+  const handleEditorChange = useCallback((editorState: EditorState) => {
+    if (!editorState?.isEmpty())
+      handleNodeDataUpdateWithSyncDraft({ id, data: { text: JSON.stringify(editorState) } })
+    else
+      handleNodeDataUpdateWithSyncDraft({ id, data: { text: '' } })
+  }, [handleNodeDataUpdateWithSyncDraft, id])
+
+  const handleShowAuthorChange = useCallback((showAuthor: boolean) => {
+    handleNodeDataUpdateWithSyncDraft({ id, data: { showAuthor } })
+  }, [handleNodeDataUpdateWithSyncDraft, id])
+
+  return {
+    handleThemeChange,
+    handleEditorChange,
+    handleShowAuthorChange,
+  }
+}

+ 127 - 0
web/app/components/workflow/note-node/index.tsx

@@ -0,0 +1,127 @@
+import {
+  memo,
+  useCallback,
+  useRef,
+} from 'react'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { useClickAway } from 'ahooks'
+import type { NodeProps } from 'reactflow'
+import NodeResizer from '../nodes/_base/components/node-resizer'
+import {
+  useNodeDataUpdate,
+  useNodesInteractions,
+} from '../hooks'
+import { useStore } from '../store'
+import {
+  NoteEditor,
+  NoteEditorContextProvider,
+  NoteEditorToolbar,
+} from './note-editor'
+import { THEME_MAP } from './constants'
+import { useNote } from './hooks'
+import type { NoteNodeType } from './types'
+
+const Icon = () => {
+  return (
+    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
+      <path fillRule="evenodd" clipRule="evenodd" d="M12 9.75V6H13.5V9.75C13.5 11.8211 11.8211 13.5 9.75 13.5H6V12H9.75C10.9926 12 12 10.9926 12 9.75Z" fill="black" fillOpacity="0.16"/>
+    </svg>
+  )
+}
+
+const NoteNode = ({
+  id,
+  data,
+}: NodeProps<NoteNodeType>) => {
+  const { t } = useTranslation()
+  const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
+  const ref = useRef<HTMLDivElement | null>(null)
+  const theme = data.theme
+  const {
+    handleThemeChange,
+    handleEditorChange,
+    handleShowAuthorChange,
+  } = useNote(id)
+  const {
+    handleNodesCopy,
+    handleNodesDuplicate,
+    handleNodeDelete,
+  } = useNodesInteractions()
+  const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
+
+  const handleDeleteNode = useCallback(() => {
+    handleNodeDelete(id)
+  }, [id, handleNodeDelete])
+
+  useClickAway(() => {
+    handleNodeDataUpdateWithSyncDraft({ id, data: { selected: false } })
+  }, ref)
+
+  return (
+    <div
+      className={cn(
+        'flex flex-col relative rounded-md shadow-xs border hover:shadow-md',
+      )}
+      style={{
+        background: THEME_MAP[theme].bg,
+        borderColor: data.selected ? THEME_MAP[theme].border : 'rgba(0, 0, 0, 0.05)',
+        width: data.width,
+        height: data.height,
+      }}
+      ref={ref}
+    >
+      <NoteEditorContextProvider
+        key={controlPromptEditorRerenderKey}
+        value={data.text}
+      >
+        <>
+          <NodeResizer
+            nodeId={id}
+            nodeData={data}
+            icon={<Icon />}
+            minWidth={240}
+            maxWidth={640}
+            minHeight={88}
+          />
+          <div className='shrink-0 h-2 opacity-50 rounded-t-md' style={{ background: THEME_MAP[theme].title }}></div>
+          {
+            data.selected && (
+              <div className='absolute -top-[41px] left-1/2 -translate-x-1/2'>
+                <NoteEditorToolbar
+                  theme={theme}
+                  onThemeChange={handleThemeChange}
+                  onCopy={handleNodesCopy}
+                  onDuplicate={handleNodesDuplicate}
+                  onDelete={handleDeleteNode}
+                  showAuthor={data.showAuthor}
+                  onShowAuthorChange={handleShowAuthorChange}
+                />
+              </div>
+            )
+          }
+          <div className='grow px-3 py-2.5 overflow-y-auto'>
+            <div className={cn(
+              data.selected && 'nodrag nopan nowheel cursor-text',
+            )}>
+              <NoteEditor
+                containerElement={ref.current}
+                placeholder={t('workflow.nodes.note.editor.placeholder') || ''}
+                onChange={handleEditorChange}
+              />
+            </div>
+          </div>
+          {
+            data.showAuthor && (
+              <div className='p-3 pt-0 text-xs text-black/[0.32]'>
+                {data.author}
+              </div>
+            )
+          }
+        </>
+      </NoteEditorContextProvider>
+    </div>
+  )
+}
+
+export default memo(NoteNode)

+ 65 - 0
web/app/components/workflow/note-node/note-editor/context.tsx

@@ -0,0 +1,65 @@
+'use client'
+
+import {
+  createContext,
+  memo,
+  useRef,
+} from 'react'
+import { LexicalComposer } from '@lexical/react/LexicalComposer'
+import { LinkNode } from '@lexical/link'
+import {
+  ListItemNode,
+  ListNode,
+} from '@lexical/list'
+import { createNoteEditorStore } from './store'
+import theme from './theme'
+
+type NoteEditorStore = ReturnType<typeof createNoteEditorStore>
+const NoteEditorContext = createContext<NoteEditorStore | null>(null)
+
+type NoteEditorContextProviderProps = {
+  value: string
+  children: JSX.Element | string | (JSX.Element | string)[]
+}
+export const NoteEditorContextProvider = memo(({
+  value,
+  children,
+}: NoteEditorContextProviderProps) => {
+  const storeRef = useRef<NoteEditorStore>()
+
+  if (!storeRef.current)
+    storeRef.current = createNoteEditorStore()
+
+  let initialValue = null
+  try {
+    initialValue = JSON.parse(value)
+  }
+  catch (e) {
+
+  }
+
+  const initialConfig = {
+    namespace: 'note-editor',
+    nodes: [
+      LinkNode,
+      ListNode,
+      ListItemNode,
+    ],
+    editorState: !initialValue?.root.children.length ? null : JSON.stringify(initialValue),
+    onError: (error: Error) => {
+      throw error
+    },
+    theme,
+  }
+
+  return (
+    <NoteEditorContext.Provider value={storeRef.current}>
+      <LexicalComposer initialConfig={{ ...initialConfig }}>
+        {children}
+      </LexicalComposer>
+    </NoteEditorContext.Provider>
+  )
+})
+NoteEditorContextProvider.displayName = 'NoteEditorContextProvider'
+
+export default NoteEditorContext

+ 62 - 0
web/app/components/workflow/note-node/note-editor/editor.tsx

@@ -0,0 +1,62 @@
+'use client'
+
+import {
+  memo,
+  useCallback,
+} from 'react'
+import type { EditorState } from 'lexical'
+import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
+import { ContentEditable } from '@lexical/react/LexicalContentEditable'
+import { ClickableLinkPlugin } from '@lexical/react/LexicalClickableLinkPlugin'
+import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'
+import { ListPlugin } from '@lexical/react/LexicalListPlugin'
+import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
+import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
+import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
+import LinkEditorPlugin from './plugins/link-editor-plugin'
+import FormatDetectorPlugin from './plugins/format-detector-plugin'
+// import TreeView from '@/app/components/base/prompt-editor/plugins/tree-view'
+import Placeholder from '@/app/components/base/prompt-editor/plugins/placeholder'
+
+type EditorProps = {
+  placeholder?: string
+  onChange?: (editorState: EditorState) => void
+  containerElement: HTMLDivElement | null
+}
+const Editor = ({
+  placeholder = 'write you note...',
+  onChange,
+  containerElement,
+}: EditorProps) => {
+  const handleEditorChange = useCallback((editorState: EditorState) => {
+    onChange?.(editorState)
+  }, [onChange])
+
+  return (
+    <div className='relative'>
+      <RichTextPlugin
+        contentEditable={
+          <div>
+            <ContentEditable
+              spellCheck={false}
+              className='w-full h-full outline-none caret-primary-600'
+              placeholder={placeholder}
+            />
+          </div>
+        }
+        placeholder={<Placeholder value={placeholder} compact />}
+        ErrorBoundary={LexicalErrorBoundary}
+      />
+      <ClickableLinkPlugin disabled />
+      <LinkPlugin />
+      <ListPlugin />
+      <LinkEditorPlugin containerElement={containerElement} />
+      <FormatDetectorPlugin />
+      <HistoryPlugin />
+      <OnChangePlugin onChange={handleEditorChange} />
+      {/* <TreeView /> */}
+    </div>
+  )
+}
+
+export default memo(Editor)

+ 3 - 0
web/app/components/workflow/note-node/note-editor/index.tsx

@@ -0,0 +1,3 @@
+export { NoteEditorContextProvider } from './context'
+export { default as NoteEditor } from './editor'
+export { default as NoteEditorToolbar } from './toolbar'

+ 78 - 0
web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/hooks.ts

@@ -0,0 +1,78 @@
+import {
+  useCallback,
+  useEffect,
+} from 'react'
+import {
+  $getSelection,
+  $isRangeSelection,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type { LinkNode } from '@lexical/link'
+import { $isLinkNode } from '@lexical/link'
+import { $isListItemNode } from '@lexical/list'
+import { getSelectedNode } from '../../utils'
+import { useNoteEditorStore } from '../../store'
+
+export const useFormatDetector = () => {
+  const [editor] = useLexicalComposerContext()
+  const noteEditorStore = useNoteEditorStore()
+
+  const handleFormat = useCallback(() => {
+    editor.getEditorState().read(() => {
+      if (editor.isComposing())
+        return
+
+      const selection = $getSelection()
+
+      if ($isRangeSelection(selection)) {
+        const node = getSelectedNode(selection)
+        const {
+          setSelectedIsBold,
+          setSelectedIsItalic,
+          setSelectedIsStrikeThrough,
+          setSelectedLinkUrl,
+          setSelectedIsLink,
+          setSelectedIsBullet,
+        } = noteEditorStore.getState()
+        setSelectedIsBold(selection.hasFormat('bold'))
+        setSelectedIsItalic(selection.hasFormat('italic'))
+        setSelectedIsStrikeThrough(selection.hasFormat('strikethrough'))
+        const parent = node.getParent()
+        if ($isLinkNode(parent) || $isLinkNode(node)) {
+          const linkUrl = ($isLinkNode(parent) ? parent : node as LinkNode).getURL()
+          setSelectedLinkUrl(linkUrl)
+          setSelectedIsLink(true)
+        }
+        else {
+          setSelectedLinkUrl('')
+          setSelectedIsLink(false)
+        }
+
+        if ($isListItemNode(parent) || $isListItemNode(node))
+          setSelectedIsBullet(true)
+        else
+          setSelectedIsBullet(false)
+      }
+    })
+  }, [editor, noteEditorStore])
+
+  useEffect(() => {
+    document.addEventListener('selectionchange', handleFormat)
+    return () => {
+      document.removeEventListener('selectionchange', handleFormat)
+    }
+  }, [handleFormat])
+
+  useEffect(() => {
+    return mergeRegister(
+      editor.registerUpdateListener(() => {
+        handleFormat()
+      }),
+    )
+  }, [editor, handleFormat])
+
+  return {
+    handleFormat,
+  }
+}

+ 9 - 0
web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/index.tsx

@@ -0,0 +1,9 @@
+import { useFormatDetector } from './hooks'
+
+const FormatDetectorPlugin = () => {
+  useFormatDetector()
+
+  return null
+}
+
+export default FormatDetectorPlugin

+ 152 - 0
web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx

@@ -0,0 +1,152 @@
+import {
+  memo,
+  useEffect,
+  useState,
+} from 'react'
+import { escape } from 'lodash-es'
+import {
+  FloatingPortal,
+  flip,
+  offset,
+  shift,
+  useFloating,
+} from '@floating-ui/react'
+import { useTranslation } from 'react-i18next'
+import { useClickAway } from 'ahooks'
+import cn from 'classnames'
+import { useStore } from '../../store'
+import { useLink } from './hooks'
+import Button from '@/app/components/base/button'
+import {
+  Edit03,
+  LinkBroken01,
+  LinkExternal01,
+} from '@/app/components/base/icons/src/vender/line/general'
+
+type LinkEditorComponentProps = {
+  containerElement: HTMLDivElement | null
+}
+const LinkEditorComponent = ({
+  containerElement,
+}: LinkEditorComponentProps) => {
+  const { t } = useTranslation()
+  const {
+    handleSaveLink,
+    handleUnlink,
+  } = useLink()
+  const selectedLinkUrl = useStore(s => s.selectedLinkUrl)
+  const linkAnchorElement = useStore(s => s.linkAnchorElement)
+  const linkOperatorShow = useStore(s => s.linkOperatorShow)
+  const setLinkAnchorElement = useStore(s => s.setLinkAnchorElement)
+  const setLinkOperatorShow = useStore(s => s.setLinkOperatorShow)
+  const [url, setUrl] = useState(selectedLinkUrl)
+  const { refs, floatingStyles, elements } = useFloating({
+    placement: 'top',
+    middleware: [
+      offset(4),
+      shift(),
+      flip(),
+    ],
+  })
+
+  useClickAway(() => {
+    setLinkAnchorElement()
+  }, linkAnchorElement)
+
+  useEffect(() => {
+    setUrl(selectedLinkUrl)
+  }, [selectedLinkUrl])
+
+  useEffect(() => {
+    if (linkAnchorElement)
+      refs.setReference(linkAnchorElement)
+  }, [linkAnchorElement, refs])
+
+  return (
+    <>
+      {
+        elements.reference && (
+          <FloatingPortal root={containerElement}>
+            <div
+              className={cn(
+                'nodrag nopan inline-flex items-center w-max rounded-md border-[0.5px] border-black/5 bg-white z-10',
+                !linkOperatorShow && 'p-1 shadow-md',
+                linkOperatorShow && 'p-0.5 shadow-sm text-xs text-gray-500 font-medium',
+              )}
+              style={floatingStyles}
+              ref={refs.setFloating}
+            >
+              {
+                !linkOperatorShow && (
+                  <>
+                    <input
+                      className='mr-0.5 p-1 w-[196px] h-6 rounded-sm text-[13px] appearance-none outline-none'
+                      value={url}
+                      onChange={e => setUrl(e.target.value)}
+                      placeholder={t('workflow.nodes.note.editor.enterUrl') || ''}
+                      autoFocus
+                    />
+                    <Button
+                      type='primary'
+                      className={cn(
+                        'py-0 px-2 h-6 text-xs',
+                        !url && 'cursor-not-allowed',
+                      )}
+                      disabled={!url}
+                      onClick={() => handleSaveLink(url)}
+                    >
+                      {t('common.operation.ok')}
+                    </Button>
+                  </>
+                )
+              }
+              {
+                linkOperatorShow && (
+                  <>
+                    <a
+                      className='flex items-center px-2 h-6 rounded-md hover:bg-gray-50'
+                      href={escape(url)}
+                      target='_blank'
+                      rel='noreferrer'
+                    >
+                      <LinkExternal01 className='mr-1 w-3 h-3' />
+                      <div className='mr-1'>
+                        {t('workflow.nodes.note.editor.openLink')}
+                      </div>
+                      <div
+                        title={escape(url)}
+                        className='text-primary-600 max-w-[140px] truncate'
+                      >
+                        {escape(url)}
+                      </div>
+                    </a>
+                    <div className='mx-1 w-[1px] h-3.5 bg-gray-100'></div>
+                    <div
+                      className='flex items-center mr-0.5 px-2 h-6 rounded-md cursor-pointer hover:bg-gray-50'
+                      onClick={(e) => {
+                        e.stopPropagation()
+                        setLinkOperatorShow(false)
+                      }}
+                    >
+                      <Edit03 className='mr-1 w-3 h-3' />
+                      {t('common.operation.edit')}
+                    </div>
+                    <div
+                      className='flex items-center px-2 h-6 rounded-md cursor-pointer hover:bg-gray-50'
+                      onClick={handleUnlink}
+                    >
+                      <LinkBroken01 className='mr-1 w-3 h-3' />
+                      {t('workflow.nodes.note.editor.unlink')}
+                    </div>
+                  </>
+                )
+              }
+            </div>
+          </FloatingPortal>
+        )
+      }
+    </>
+  )
+}
+
+export default memo(LinkEditorComponent)

+ 115 - 0
web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts

@@ -0,0 +1,115 @@
+import {
+  useCallback,
+  useEffect,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  CLICK_COMMAND,
+  COMMAND_PRIORITY_LOW,
+} from 'lexical'
+import {
+  mergeRegister,
+} from '@lexical/utils'
+import {
+  TOGGLE_LINK_COMMAND,
+} from '@lexical/link'
+import { escape } from 'lodash-es'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { useNoteEditorStore } from '../../store'
+import { urlRegExp } from '../../utils'
+import { useToastContext } from '@/app/components/base/toast'
+
+export const useOpenLink = () => {
+  const [editor] = useLexicalComposerContext()
+  const noteEditorStore = useNoteEditorStore()
+
+  useEffect(() => {
+    return mergeRegister(
+      editor.registerUpdateListener(() => {
+        setTimeout(() => {
+          const {
+            selectedLinkUrl,
+            selectedIsLink,
+            setLinkAnchorElement,
+            setLinkOperatorShow,
+          } = noteEditorStore.getState()
+
+          if (selectedIsLink) {
+            setLinkAnchorElement(true)
+
+            if (selectedLinkUrl)
+              setLinkOperatorShow(true)
+            else
+              setLinkOperatorShow(false)
+          }
+          else {
+            setLinkAnchorElement()
+            setLinkOperatorShow(false)
+          }
+        })
+      }),
+      editor.registerCommand(
+        CLICK_COMMAND,
+        (payload) => {
+          setTimeout(() => {
+            const {
+              selectedLinkUrl,
+              selectedIsLink,
+              setLinkAnchorElement,
+              setLinkOperatorShow,
+            } = noteEditorStore.getState()
+
+            if (selectedIsLink) {
+              if ((payload.metaKey || payload.ctrlKey) && selectedLinkUrl) {
+                window.open(selectedLinkUrl, '_blank')
+                return true
+              }
+              setLinkAnchorElement(true)
+
+              if (selectedLinkUrl)
+                setLinkOperatorShow(true)
+              else
+                setLinkOperatorShow(false)
+            }
+            else {
+              setLinkAnchorElement()
+              setLinkOperatorShow(false)
+            }
+          })
+          return false
+        },
+        COMMAND_PRIORITY_LOW,
+      ),
+    )
+  }, [editor, noteEditorStore])
+}
+
+export const useLink = () => {
+  const { t } = useTranslation()
+  const [editor] = useLexicalComposerContext()
+  const noteEditorStore = useNoteEditorStore()
+  const { notify } = useToastContext()
+
+  const handleSaveLink = useCallback((url: string) => {
+    if (url && !urlRegExp.test(url)) {
+      notify({ type: 'error', message: t('workflow.nodes.note.editor.invalidUrl') })
+      return
+    }
+    editor.dispatchCommand(TOGGLE_LINK_COMMAND, escape(url))
+
+    const { setLinkAnchorElement } = noteEditorStore.getState()
+    setLinkAnchorElement()
+  }, [editor, noteEditorStore, notify, t])
+
+  const handleUnlink = useCallback(() => {
+    editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
+
+    const { setLinkAnchorElement } = noteEditorStore.getState()
+    setLinkAnchorElement()
+  }, [editor, noteEditorStore])
+
+  return {
+    handleSaveLink,
+    handleUnlink,
+  }
+}

+ 25 - 0
web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/index.tsx

@@ -0,0 +1,25 @@
+import {
+  memo,
+} from 'react'
+import { useStore } from '../../store'
+import { useOpenLink } from './hooks'
+import LinkEditorComponent from './component'
+
+type LinkEditorPluginProps = {
+  containerElement: HTMLDivElement | null
+}
+const LinkEditorPlugin = ({
+  containerElement,
+}: LinkEditorPluginProps) => {
+  useOpenLink()
+  const linkAnchorElement = useStore(s => s.linkAnchorElement)
+
+  if (!linkAnchorElement)
+    return null
+
+  return (
+    <LinkEditorComponent containerElement={containerElement} />
+  )
+}
+
+export default memo(LinkEditorPlugin)

+ 72 - 0
web/app/components/workflow/note-node/note-editor/store.ts

@@ -0,0 +1,72 @@
+import { useContext } from 'react'
+import {
+  useStore as useZustandStore,
+} from 'zustand'
+import { createStore } from 'zustand/vanilla'
+import NoteEditorContext from './context'
+
+type Shape = {
+  linkAnchorElement: HTMLElement | null
+  setLinkAnchorElement: (open?: boolean) => void
+  linkOperatorShow: boolean
+  setLinkOperatorShow: (linkOperatorShow: boolean) => void
+  selectedIsBold: boolean
+  setSelectedIsBold: (selectedIsBold: boolean) => void
+  selectedIsItalic: boolean
+  setSelectedIsItalic: (selectedIsItalic: boolean) => void
+  selectedIsStrikeThrough: boolean
+  setSelectedIsStrikeThrough: (selectedIsStrikeThrough: boolean) => void
+  selectedLinkUrl: string
+  setSelectedLinkUrl: (selectedLinkUrl: string) => void
+  selectedIsLink: boolean
+  setSelectedIsLink: (selectedIsLink: boolean) => void
+  selectedIsBullet: boolean
+  setSelectedIsBullet: (selectedIsBullet: boolean) => void
+}
+
+export const createNoteEditorStore = () => {
+  return createStore<Shape>(set => ({
+    linkAnchorElement: null,
+    setLinkAnchorElement: (open) => {
+      if (open) {
+        setTimeout(() => {
+          const nativeSelection = window.getSelection()
+
+          if (nativeSelection?.focusNode) {
+            const parent = nativeSelection.focusNode.parentElement
+            set(() => ({ linkAnchorElement: parent }))
+          }
+        })
+      }
+      else {
+        set(() => ({ linkAnchorElement: null }))
+      }
+    },
+    linkOperatorShow: false,
+    setLinkOperatorShow: linkOperatorShow => set(() => ({ linkOperatorShow })),
+    selectedIsBold: false,
+    setSelectedIsBold: selectedIsBold => set(() => ({ selectedIsBold })),
+    selectedIsItalic: false,
+    setSelectedIsItalic: selectedIsItalic => set(() => ({ selectedIsItalic })),
+    selectedIsStrikeThrough: false,
+    setSelectedIsStrikeThrough: selectedIsStrikeThrough => set(() => ({ selectedIsStrikeThrough })),
+    selectedLinkUrl: '',
+    setSelectedLinkUrl: selectedLinkUrl => set(() => ({ selectedLinkUrl })),
+    selectedIsLink: false,
+    setSelectedIsLink: selectedIsLink => set(() => ({ selectedIsLink })),
+    selectedIsBullet: false,
+    setSelectedIsBullet: selectedIsBullet => set(() => ({ selectedIsBullet })),
+  }))
+}
+
+export function useStore<T>(selector: (state: Shape) => T): T {
+  const store = useContext(NoteEditorContext)
+  if (!store)
+    throw new Error('Missing NoteEditorContext.Provider in the tree')
+
+  return useZustandStore(store, selector)
+}
+
+export const useNoteEditorStore = () => {
+  return useContext(NoteEditorContext)!
+}

+ 17 - 0
web/app/components/workflow/note-node/note-editor/theme/index.ts

@@ -0,0 +1,17 @@
+import type { EditorThemeClasses } from 'lexical'
+
+import './theme.css'
+
+const theme: EditorThemeClasses = {
+  paragraph: 'note-editor-theme_paragraph',
+  list: {
+    ul: 'note-editor-theme_list-ul',
+    listitem: 'note-editor-theme_list-li',
+  },
+  link: 'note-editor-theme_link',
+  text: {
+    strikethrough: 'note-editor-theme_text-strikethrough',
+  },
+}
+
+export default theme

+ 24 - 0
web/app/components/workflow/note-node/note-editor/theme/theme.css

@@ -0,0 +1,24 @@
+.note-editor-theme_paragraph {
+  font-size: 12px;
+}
+
+.note-editor-theme_list-ul {
+  font-size: 12px;
+  margin: 0;
+  padding: 0;
+  list-style: disc;
+}
+
+.note-editor-theme_list-li {
+  margin-left: 18px;
+  margin-right: 8px;
+}
+
+.note-editor-theme_link {
+  text-decoration: underline;
+  cursor: pointer;
+}
+
+.note-editor-theme_text-strikethrough {
+  text-decoration: line-through;
+}

+ 105 - 0
web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx

@@ -0,0 +1,105 @@
+import {
+  memo,
+  useState,
+} from 'react'
+import cn from 'classnames'
+import { NoteTheme } from '../../types'
+import { THEME_MAP } from '../../constants'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+
+export const COLOR_LIST = [
+  {
+    key: NoteTheme.blue,
+    inner: THEME_MAP[NoteTheme.blue].title,
+    outer: THEME_MAP[NoteTheme.blue].outer,
+  },
+  {
+    key: NoteTheme.cyan,
+    inner: THEME_MAP[NoteTheme.cyan].title,
+    outer: THEME_MAP[NoteTheme.cyan].outer,
+  },
+  {
+    key: NoteTheme.green,
+    inner: THEME_MAP[NoteTheme.green].title,
+    outer: THEME_MAP[NoteTheme.green].outer,
+  },
+  {
+    key: NoteTheme.yellow,
+    inner: THEME_MAP[NoteTheme.yellow].title,
+    outer: THEME_MAP[NoteTheme.yellow].outer,
+  },
+  {
+    key: NoteTheme.pink,
+    inner: THEME_MAP[NoteTheme.pink].title,
+    outer: THEME_MAP[NoteTheme.pink].outer,
+  },
+  {
+    key: NoteTheme.violet,
+    inner: THEME_MAP[NoteTheme.violet].title,
+    outer: THEME_MAP[NoteTheme.violet].outer,
+  },
+]
+
+export type ColorPickerProps = {
+  theme: NoteTheme
+  onThemeChange: (theme: NoteTheme) => void
+}
+const ColorPicker = ({
+  theme,
+  onThemeChange,
+}: ColorPickerProps) => {
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='top'
+      offset={4}
+    >
+      <PortalToFollowElemTrigger onClick={() => setOpen(!open)}>
+        <div className={cn(
+          'flex items-center justify-center w-8 h-8 rounded-md cursor-pointer hover:bg-black/5',
+          open && 'bg-black/5',
+        )}>
+          <div
+            className='w-4 h-4 rounded-full border border-black/5'
+            style={{ backgroundColor: THEME_MAP[theme].title }}
+          ></div>
+        </div>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent>
+        <div className='grid grid-cols-3 grid-rows-2 gap-0.5 p-0.5 rounded-lg border-[0.5px] border-black/8 bg-white shadow-lg'>
+          {
+            COLOR_LIST.map(color => (
+              <div
+                key={color.key}
+                className='group relative flex items-center justify-center w-8 h-8 rounded-md cursor-pointer'
+                onClick={(e) => {
+                  e.stopPropagation()
+                  onThemeChange(color.key)
+                  setOpen(false)
+                }}
+              >
+                <div
+                  className='hidden group-hover:block absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-5 h-5 rounded-full border-[1.5px]'
+                  style={{ borderColor: color.outer }}
+                ></div>
+                <div
+                  className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 rounded-full border border-black/5'
+                  style={{ backgroundColor: color.inner }}
+                ></div>
+              </div>
+            ))
+          }
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+
+export default memo(ColorPicker)

+ 81 - 0
web/app/components/workflow/note-node/note-editor/toolbar/command.tsx

@@ -0,0 +1,81 @@
+import {
+  memo,
+  useMemo,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import { useStore } from '../store'
+import { useCommand } from './hooks'
+import { Link01 } from '@/app/components/base/icons/src/vender/line/general'
+import {
+  Bold01,
+  Dotpoints01,
+  Italic01,
+  Strikethrough01,
+} from '@/app/components/base/icons/src/vender/line/editor'
+import TooltipPlus from '@/app/components/base/tooltip-plus'
+
+type CommandProps = {
+  type: 'bold' | 'italic' | 'strikethrough' | 'link' | 'bullet'
+}
+const Command = ({
+  type,
+}: CommandProps) => {
+  const { t } = useTranslation()
+  const selectedIsBold = useStore(s => s.selectedIsBold)
+  const selectedIsItalic = useStore(s => s.selectedIsItalic)
+  const selectedIsStrikeThrough = useStore(s => s.selectedIsStrikeThrough)
+  const selectedIsLink = useStore(s => s.selectedIsLink)
+  const selectedIsBullet = useStore(s => s.selectedIsBullet)
+  const { handleCommand } = useCommand()
+
+  const icon = useMemo(() => {
+    switch (type) {
+      case 'bold':
+        return <Bold01 className={cn('w-4 h-4', selectedIsBold && 'text-primary-600')} />
+      case 'italic':
+        return <Italic01 className={cn('w-4 h-4', selectedIsItalic && 'text-primary-600')} />
+      case 'strikethrough':
+        return <Strikethrough01 className={cn('w-4 h-4', selectedIsStrikeThrough && 'text-primary-600')} />
+      case 'link':
+        return <Link01 className={cn('w-4 h-4', selectedIsLink && 'text-primary-600')} />
+      case 'bullet':
+        return <Dotpoints01 className={cn('w-4 h-4', selectedIsBullet && 'text-primary-600')} />
+    }
+  }, [type, selectedIsBold, selectedIsItalic, selectedIsStrikeThrough, selectedIsLink, selectedIsBullet])
+
+  const tip = useMemo(() => {
+    switch (type) {
+      case 'bold':
+        return t('workflow.nodes.note.editor.bold')
+      case 'italic':
+        return t('workflow.nodes.note.editor.italic')
+      case 'strikethrough':
+        return t('workflow.nodes.note.editor.strikethrough')
+      case 'link':
+        return t('workflow.nodes.note.editor.link')
+      case 'bullet':
+        return t('workflow.nodes.note.editor.bulletList')
+    }
+  }, [type, t])
+
+  return (
+    <TooltipPlus popupContent={tip}>
+      <div
+        className={cn(
+          'flex items-center justify-center w-8 h-8 cursor-pointer rounded-md text-gray-500 hover:text-gray-800 hover:bg-black/5',
+          type === 'bold' && selectedIsBold && 'bg-primary-50',
+          type === 'italic' && selectedIsItalic && 'bg-primary-50',
+          type === 'strikethrough' && selectedIsStrikeThrough && 'bg-primary-50',
+          type === 'link' && selectedIsLink && 'bg-primary-50',
+          type === 'bullet' && selectedIsBullet && 'bg-primary-50',
+        )}
+        onClick={() => handleCommand(type)}
+      >
+        {icon}
+      </div>
+    </TooltipPlus>
+  )
+}
+
+export default memo(Command)

+ 7 - 0
web/app/components/workflow/note-node/note-editor/toolbar/divider.tsx

@@ -0,0 +1,7 @@
+const Divider = () => {
+  return (
+    <div className='mx-1 w-[1px] h-3.5 bg-gray-200'></div>
+  )
+}
+
+export default Divider

+ 86 - 0
web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx

@@ -0,0 +1,86 @@
+import { memo } from 'react'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { useFontSize } from './hooks'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import { TitleCase } from '@/app/components/base/icons/src/vender/line/editor'
+import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
+import { Check } from '@/app/components/base/icons/src/vender/line/general'
+
+const FontSizeSelector = () => {
+  const { t } = useTranslation()
+  const FONT_SIZE_LIST = [
+    {
+      key: '12px',
+      value: t('workflow.nodes.note.editor.small'),
+    },
+    {
+      key: '14px',
+      value: t('workflow.nodes.note.editor.medium'),
+    },
+    {
+      key: '16px',
+      value: t('workflow.nodes.note.editor.large'),
+    },
+  ]
+  const {
+    fontSizeSelectorShow,
+    handleOpenFontSizeSelector,
+    fontSize,
+    handleFontSize,
+  } = useFontSize()
+
+  return (
+    <PortalToFollowElem
+      open={fontSizeSelectorShow}
+      onOpenChange={handleOpenFontSizeSelector}
+      placement='bottom-start'
+      offset={2}
+    >
+      <PortalToFollowElemTrigger onClick={() => handleOpenFontSizeSelector(!fontSizeSelectorShow)}>
+        <div className={cn(
+          'flex items-center pl-2 pr-1.5 h-8 rounded-md text-[13px] font-medium text-gray-700 cursor-pointer hover:bg-gray-50',
+          fontSizeSelectorShow && 'bg-gray-50',
+        )}>
+          <TitleCase className='mr-1 w-4 h-4' />
+          {FONT_SIZE_LIST.find(font => font.key === fontSize)?.value || t('workflow.nodes.note.editor.small')}
+          <ChevronDown className='ml-0.5 w-3 h-3' />
+        </div>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent>
+        <div className='p-1 w-[120px] bg-white border-[0.5px] border-gray-200 rounded-md shadow-xl text-gray-700'>
+          {
+            FONT_SIZE_LIST.map(font => (
+              <div
+                key={font.key}
+                className='flex items-center justify-between pl-3 pr-2 h-8 rounded-md cursor-pointer hover:bg-gray-50'
+                onClick={(e) => {
+                  e.stopPropagation()
+                  handleFontSize(font.key)
+                  handleOpenFontSizeSelector(false)
+                }}
+              >
+                <div
+                  style={{ fontSize: font.key }}
+                >
+                  {font.value}
+                </div>
+                {
+                  fontSize === font.key && (
+                    <Check className='w-4 h-4 text-primary-500' />
+                  )
+                }
+              </div>
+            ))
+          }
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+
+export default memo(FontSizeSelector)

+ 147 - 0
web/app/components/workflow/note-node/note-editor/toolbar/hooks.ts

@@ -0,0 +1,147 @@
+import {
+  useCallback,
+  useEffect,
+  useState,
+} from 'react'
+import {
+  $createParagraphNode,
+  $getSelection,
+  $isRangeSelection,
+  $setSelection,
+  COMMAND_PRIORITY_CRITICAL,
+  FORMAT_TEXT_COMMAND,
+  SELECTION_CHANGE_COMMAND,
+} from 'lexical'
+import {
+  $getSelectionStyleValueForProperty,
+  $patchStyleText,
+  $setBlocksType,
+} from '@lexical/selection'
+import { INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list'
+import { mergeRegister } from '@lexical/utils'
+import {
+  $isLinkNode,
+  TOGGLE_LINK_COMMAND,
+} from '@lexical/link'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { useNoteEditorStore } from '../store'
+import { getSelectedNode } from '../utils'
+
+export const useCommand = () => {
+  const [editor] = useLexicalComposerContext()
+  const noteEditorStore = useNoteEditorStore()
+
+  const handleCommand = useCallback((type: string) => {
+    if (type === 'bold')
+      editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')
+
+    if (type === 'italic')
+      editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')
+
+    if (type === 'strikethrough')
+      editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')
+
+    if (type === 'link') {
+      editor.update(() => {
+        const selection = $getSelection()
+
+        if ($isRangeSelection(selection)) {
+          const node = getSelectedNode(selection)
+          const parent = node.getParent()
+          const { setLinkAnchorElement } = noteEditorStore.getState()
+
+          if ($isLinkNode(parent) || $isLinkNode(node)) {
+            editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
+            setLinkAnchorElement()
+          }
+          else {
+            editor.dispatchCommand(TOGGLE_LINK_COMMAND, '')
+            setLinkAnchorElement(true)
+          }
+        }
+      })
+    }
+
+    if (type === 'bullet') {
+      const { selectedIsBullet } = noteEditorStore.getState()
+
+      if (selectedIsBullet) {
+        editor.update(() => {
+          const selection = $getSelection()
+          if ($isRangeSelection(selection))
+            $setBlocksType(selection, () => $createParagraphNode())
+        })
+      }
+      else {
+        editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
+      }
+    }
+  }, [editor, noteEditorStore])
+
+  return {
+    handleCommand,
+  }
+}
+
+export const useFontSize = () => {
+  const [editor] = useLexicalComposerContext()
+  const [fontSize, setFontSize] = useState('12px')
+  const [fontSizeSelectorShow, setFontSizeSelectorShow] = useState(false)
+
+  const handleFontSize = useCallback((fontSize: string) => {
+    editor.update(() => {
+      const selection = $getSelection()
+
+      if ($isRangeSelection(selection))
+        $patchStyleText(selection, { 'font-size': fontSize })
+    })
+  }, [editor])
+
+  const handleOpenFontSizeSelector = useCallback((newFontSizeSelectorShow: boolean) => {
+    if (newFontSizeSelectorShow) {
+      editor.update(() => {
+        const selection = $getSelection()
+
+        if ($isRangeSelection(selection))
+          $setSelection(selection.clone())
+      })
+    }
+    setFontSizeSelectorShow(newFontSizeSelectorShow)
+  }, [editor])
+
+  useEffect(() => {
+    return mergeRegister(
+      editor.registerUpdateListener(() => {
+        editor.getEditorState().read(() => {
+          const selection = $getSelection()
+
+          if ($isRangeSelection(selection)) {
+            const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', '12px')
+            setFontSize(fontSize)
+          }
+        })
+      }),
+      editor.registerCommand(
+        SELECTION_CHANGE_COMMAND,
+        () => {
+          const selection = $getSelection()
+
+          if ($isRangeSelection(selection)) {
+            const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', '12px')
+            setFontSize(fontSize)
+          }
+
+          return false
+        },
+        COMMAND_PRIORITY_CRITICAL,
+      ),
+    )
+  }, [editor])
+
+  return {
+    fontSize,
+    handleFontSize,
+    fontSizeSelectorShow,
+    handleOpenFontSizeSelector,
+  }
+}

+ 48 - 0
web/app/components/workflow/note-node/note-editor/toolbar/index.tsx

@@ -0,0 +1,48 @@
+import { memo } from 'react'
+import Divider from './divider'
+import type { ColorPickerProps } from './color-picker'
+import ColorPicker from './color-picker'
+import FontSizeSelector from './font-size-selector'
+import Command from './command'
+import type { OperatorProps } from './operator'
+import Operator from './operator'
+
+type ToolbarProps = ColorPickerProps & OperatorProps
+const Toolbar = ({
+  theme,
+  onThemeChange,
+  onCopy,
+  onDuplicate,
+  onDelete,
+  showAuthor,
+  onShowAuthorChange,
+}: ToolbarProps) => {
+  return (
+    <div className='inline-flex items-center p-0.5 bg-white rounded-lg border-[0.5px] border-black/5 shadow-sm'>
+      <ColorPicker
+        theme={theme}
+        onThemeChange={onThemeChange}
+      />
+      <Divider />
+      <FontSizeSelector />
+      <Divider />
+      <div className='flex items-center space-x-0.5'>
+        <Command type='bold' />
+        <Command type='italic' />
+        <Command type='strikethrough' />
+        <Command type='link' />
+        <Command type='bullet' />
+      </div>
+      <Divider />
+      <Operator
+        onCopy={onCopy}
+        onDuplicate={onDuplicate}
+        onDelete={onDelete}
+        showAuthor={showAuthor}
+        onShowAuthorChange={onShowAuthorChange}
+      />
+    </div>
+  )
+}
+
+export default memo(Toolbar)

+ 107 - 0
web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx

@@ -0,0 +1,107 @@
+import {
+  memo,
+  useState,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import ShortcutsName from '@/app/components/workflow/shortcuts-name'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
+import Switch from '@/app/components/base/switch'
+
+export type OperatorProps = {
+  onCopy: () => void
+  onDuplicate: () => void
+  onDelete: () => void
+  showAuthor: boolean
+  onShowAuthorChange: (showAuthor: boolean) => void
+}
+const Operator = ({
+  onCopy,
+  onDelete,
+  onDuplicate,
+  showAuthor,
+  onShowAuthorChange,
+}: OperatorProps) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-end'
+      offset={4}
+    >
+      <PortalToFollowElemTrigger onClick={() => setOpen(!open)}>
+        <div
+          className={cn(
+            'flex items-center justify-center w-8 h-8 cursor-pointer rounded-lg hover:bg-black/5',
+            open && 'bg-black/5',
+          )}
+        >
+          <DotsHorizontal className='w-4 h-4 text-gray-500' />
+        </div>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent>
+        <div className='min-w-[192px] bg-white rounded-md border-[0.5px] border-gray-200 shadow-xl'>
+          <div className='p-1'>
+            <div
+              className='flex items-center justify-between px-3 h-8 cursor-pointer rounded-md text-sm text-gray-700 hover:bg-black/5'
+              onClick={() => {
+                onCopy()
+                setOpen(false)
+              }}
+            >
+              {t('workflow.common.copy')}
+              <ShortcutsName keys={['ctrl', 'c']} />
+            </div>
+            <div
+              className='flex items-center justify-between px-3 h-8 cursor-pointer rounded-md text-sm text-gray-700 hover:bg-black/5'
+              onClick={() => {
+                onDuplicate()
+                setOpen(false)
+              }}
+            >
+              {t('workflow.common.duplicate')}
+              <ShortcutsName keys={['ctrl', 'd']} />
+            </div>
+          </div>
+          <div className='h-[1px] bg-gray-100'></div>
+          <div className='p-1'>
+            <div
+              className='flex items-center justify-between px-3 h-8 cursor-pointer rounded-md text-sm text-gray-700 hover:bg-black/5'
+              onClick={e => e.stopPropagation()}
+            >
+              <div>{t('workflow.nodes.note.editor.showAuthor')}</div>
+              <Switch
+                size='l'
+                defaultValue={showAuthor}
+                onChange={onShowAuthorChange}
+              />
+            </div>
+          </div>
+          <div className='h-[1px] bg-gray-100'></div>
+          <div className='p-1'>
+            <div
+              className='flex items-center justify-between px-3 h-8 cursor-pointer rounded-md text-sm text-gray-700 hover:text-[#D92D20] hover:bg-[#FEF3F2]'
+              onClick={() => {
+                onDelete()
+                setOpen(false)
+              }}
+            >
+              {t('common.operation.delete')}
+              <ShortcutsName keys={['del']} />
+            </div>
+          </div>
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+
+export default memo(Operator)

+ 21 - 0
web/app/components/workflow/note-node/note-editor/utils.ts

@@ -0,0 +1,21 @@
+import { $isAtNodeEnd } from '@lexical/selection'
+import type { ElementNode, RangeSelection, TextNode } from 'lexical'
+
+export function getSelectedNode(
+  selection: RangeSelection,
+): TextNode | ElementNode {
+  const anchor = selection.anchor
+  const focus = selection.focus
+  const anchorNode = selection.anchor.getNode()
+  const focusNode = selection.focus.getNode()
+  if (anchorNode === focusNode)
+    return anchorNode
+
+  const isBackward = selection.isBackward()
+  if (isBackward)
+    return $isAtNodeEnd(focus) ? anchorNode : focusNode
+  else
+    return $isAtNodeEnd(anchor) ? anchorNode : focusNode
+}
+
+export const urlRegExp = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/

+ 17 - 0
web/app/components/workflow/note-node/types.ts

@@ -0,0 +1,17 @@
+import type { CommonNodeType } from '../types'
+
+export enum NoteTheme {
+  blue = 'blue',
+  cyan = 'cyan',
+  green = 'green',
+  yellow = 'yellow',
+  pink = 'pink',
+  violet = 'violet',
+}
+
+export type NoteNodeType = CommonNodeType & {
+  text: string
+  theme: NoteTheme
+  author: string
+  showAuthor: boolean
+}

+ 27 - 1
web/app/components/workflow/operator/control.tsx

@@ -1,4 +1,8 @@
-import { memo, useCallback } from 'react'
+import type { MouseEvent } from 'react'
+import {
+  memo,
+  useCallback,
+} from 'react'
 import { useTranslation } from 'react-i18next'
 import cn from 'classnames'
 import { useKeyPress } from 'ahooks'
@@ -11,6 +15,7 @@ import { isEventTargetInputArea } from '../utils'
 import { useStore } from '../store'
 import AddBlock from './add-block'
 import TipPopup from './tip-popup'
+import { useOperator } from './hooks'
 import {
   Cursor02C,
   Hand02,
@@ -20,12 +25,14 @@ import {
   Hand02 as Hand02Solid,
 } from '@/app/components/base/icons/src/vender/solid/editor'
 import { OrganizeGrid } from '@/app/components/base/icons/src/vender/line/layout'
+import { StickerSquare } from '@/app/components/base/icons/src/vender/line/files'
 
 const Control = () => {
   const { t } = useTranslation()
   const controlMode = useStore(s => s.controlMode)
   const setControlMode = useStore(s => s.setControlMode)
   const { handleLayout } = useWorkflow()
+  const { handleAddNote } = useOperator()
   const {
     nodesReadOnly,
     getNodesReadOnly,
@@ -75,9 +82,28 @@ const Control = () => {
     handleLayout()
   }
 
+  const addNote = (e: MouseEvent<HTMLDivElement>) => {
+    if (getNodesReadOnly())
+      return
+
+    e.stopPropagation()
+    handleAddNote()
+  }
+
   return (
     <div className='flex items-center p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-lg text-gray-500'>
       <AddBlock />
+      <TipPopup title={t('workflow.nodes.note.addNote')}>
+        <div
+          className={cn(
+            'flex items-center justify-center ml-[1px] w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
+            `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
+          )}
+          onClick={addNote}
+        >
+          <StickerSquare />
+        </div>
+      </TipPopup>
       <div className='mx-[3px] w-[1px] h-3.5 bg-gray-200'></div>
       <TipPopup title={t('workflow.common.pointerMode')}>
         <div

+ 41 - 0
web/app/components/workflow/operator/hooks.ts

@@ -0,0 +1,41 @@
+import { useCallback } from 'react'
+import { generateNewNode } from '../utils'
+import { useWorkflowStore } from '../store'
+import type { NoteNodeType } from '../note-node/types'
+import { CUSTOM_NOTE_NODE } from '../note-node/constants'
+import { NoteTheme } from '../note-node/types'
+import { useAppContext } from '@/context/app-context'
+
+export const useOperator = () => {
+  const workflowStore = useWorkflowStore()
+  const { userProfile } = useAppContext()
+
+  const handleAddNote = useCallback(() => {
+    const newNode = generateNewNode({
+      type: CUSTOM_NOTE_NODE,
+      data: {
+        title: '',
+        desc: '',
+        type: '' as any,
+        text: '',
+        theme: NoteTheme.blue,
+        author: userProfile?.name || '',
+        showAuthor: true,
+        width: 240,
+        height: 88,
+        _isCandidate: true,
+      } as NoteNodeType,
+      position: {
+        x: 0,
+        y: 0,
+      },
+    })
+    workflowStore.setState({
+      candidateNode: newNode,
+    })
+  }, [workflowStore, userProfile])
+
+  return {
+    handleAddNote,
+  }
+}

+ 12 - 0
web/app/components/workflow/panel-contextmenu.tsx

@@ -13,6 +13,7 @@ import {
   useWorkflowStartRun,
 } from './hooks'
 import AddBlock from './operator/add-block'
+import { useOperator } from './operator/hooks'
 import { exportAppConfig } from '@/service/apps'
 import { useToastContext } from '@/app/components/base/toast'
 import { useStore as useAppStore } from '@/app/components/app/store'
@@ -27,6 +28,7 @@ const PanelContextmenu = () => {
   const { handleNodesPaste } = useNodesInteractions()
   const { handlePaneContextmenuCancel } = usePanelInteractions()
   const { handleStartWorkflowRun } = useWorkflowStartRun()
+  const { handleAddNote } = useOperator()
 
   useClickAway(() => {
     handlePaneContextmenuCancel()
@@ -78,6 +80,16 @@ const PanelContextmenu = () => {
             crossAxis: -4,
           }}
         />
+        <div
+          className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
+          onClick={(e) => {
+            e.stopPropagation()
+            handleAddNote()
+            handlePaneContextmenuCancel()
+          }}
+        >
+          {t('workflow.nodes.note.addNote')}
+        </div>
         <div
           className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
           onClick={() => {

+ 6 - 4
web/app/components/workflow/utils.ts

@@ -17,6 +17,7 @@ import type {
 } from './types'
 import { BlockEnum } from './types'
 import {
+  CUSTOM_NODE,
   ITERATION_NODE_Z_INDEX,
   NODE_WIDTH_X_OFFSET,
   START_INITIAL_POSITION,
@@ -105,7 +106,8 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
   }, {} as Record<string, string[]>)
 
   return nodes.map((node) => {
-    node.type = 'custom'
+    if (!node.type)
+      node.type = CUSTOM_NODE
 
     const connectedEdges = getConnectedEdges([node], edges)
     node.data._connectedSourceHandleIds = connectedEdges.filter(edge => edge.source === node.id).map(edge => edge.sourceHandle || 'source')
@@ -189,7 +191,7 @@ export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => {
 export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
   const dagreGraph = new dagre.graphlib.Graph()
   dagreGraph.setDefaultEdgeLabel(() => ({}))
-  const nodes = cloneDeep(originNodes).filter(node => !node.parentId)
+  const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
   const edges = cloneDeep(originEdges).filter(edge => !edge.data?.isInIteration)
   dagreGraph.setGraph({
     rankdir: 'LR',
@@ -280,10 +282,10 @@ export const getNodesConnectedSourceOrTargetHandleIdsMap = (changes: ConnectedSo
   return nodesConnectedSourceOrTargetHandleIdsMap
 }
 
-export const generateNewNode = ({ data, position, id, zIndex, ...rest }: Omit<Node, 'id'> & { id?: string }) => {
+export const generateNewNode = ({ data, position, id, zIndex, type, ...rest }: Omit<Node, 'id'> & { id?: string }) => {
   return {
     id: id || `${Date.now()}`,
-    type: 'custom',
+    type: type || CUSTOM_NODE,
     data,
     position,
     targetPosition: Position.Left,

+ 19 - 0
web/i18n/en-US/workflow.ts

@@ -412,6 +412,25 @@ const translation = {
       iteration_other: '{{count}} Iterations',
       currentIteration: 'Current Iteration',
     },
+    note: {
+      addNote: 'Add Note',
+      editor: {
+        placeholder: 'Write your note...',
+        small: 'Small',
+        medium: 'Medium',
+        large: 'Large',
+        bold: 'Bold',
+        italic: 'Italic',
+        strikethrough: 'Strikethrough',
+        link: 'Link',
+        openLink: 'Open',
+        unlink: 'Unlink',
+        enterUrl: 'Enter URL...',
+        invalidUrl: 'Invalid URL',
+        bulletList: 'Bullet List',
+        showAuthor: 'Show Author',
+      },
+    },
   },
   tracing: {
     stopBy: 'Stop by {{user}}',

+ 19 - 0
web/i18n/zh-Hans/workflow.ts

@@ -412,6 +412,25 @@ const translation = {
       iteration_other: '{{count}}个迭代',
       currentIteration: '当前迭代',
     },
+    note: {
+      addNote: '添加注释',
+      editor: {
+        placeholder: '输入注释...',
+        small: '小',
+        medium: '中',
+        large: '大',
+        bold: '加粗',
+        italic: '斜体',
+        strikethrough: '删除线',
+        link: '链接',
+        openLink: '打开',
+        unlink: '取消链接',
+        enterUrl: '输入链接...',
+        invalidUrl: '无效的链接',
+        bulletList: '列表',
+        showAuthor: '显示作者',
+      },
+    },
   },
   tracing: {
     stopBy: '由{{user}}终止',

+ 2 - 2
web/package.json

@@ -23,7 +23,7 @@
     "@headlessui/react": "^1.7.13",
     "@heroicons/react": "^2.0.16",
     "@hookform/resolvers": "^3.3.4",
-    "@lexical/react": "^0.12.2",
+    "@lexical/react": "^0.16.0",
     "@mdx-js/loader": "^2.3.0",
     "@mdx-js/react": "^2.3.0",
     "@monaco-editor/react": "^4.6.0",
@@ -47,7 +47,7 @@
     "js-cookie": "^3.0.1",
     "katex": "^0.16.10",
     "lamejs": "^1.2.1",
-    "lexical": "^0.12.2",
+    "lexical": "^0.16.0",
     "lodash-es": "^4.17.21",
     "mermaid": "10.4.0",
     "negotiator": "^0.6.3",

+ 171 - 124
web/yarn.lock

@@ -414,159 +414,206 @@
     "@jridgewell/resolve-uri" "3.1.0"
     "@jridgewell/sourcemap-codec" "1.4.14"
 
-"@lexical/clipboard@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.12.2.tgz"
-  integrity sha512-RldmfZquuJJJCJ5WquCyoJ1/eZ+AnNgdksqvd+G+Yn/GyJl/+O3dnHM0QVaDSPvh/PynLFcCtz/57ySLo2kQxQ==
+"@lexical/clipboard@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.16.0.tgz#3ae0d87a56bd3518de077e45b0c1bbba2f356193"
+  integrity sha512-eYMJ6jCXpWBVC05Mu9HLMysrBbfi++xFfsm+Yo7A6kYGrqYUhpXqjJkYnw1xdZYL3bV73Oe4ByVJuq42GU+Mqw==
   dependencies:
-    "@lexical/html" "0.12.2"
-    "@lexical/list" "0.12.2"
-    "@lexical/selection" "0.12.2"
-    "@lexical/utils" "0.12.2"
+    "@lexical/html" "0.16.0"
+    "@lexical/list" "0.16.0"
+    "@lexical/selection" "0.16.0"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/code@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/code/-/code-0.12.2.tgz"
-  integrity sha512-w2JeJdnMUtYnC/Fx78sL3iJBt9Ug8pFSDOcI9ay/BkMQFQV8oqq1iyuLLBBJSG4FAM8b2DXrVdGklRQ+jTfTVw==
+"@lexical/code@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.16.0.tgz#225030342e3c361e5541c750033323007a947880"
+  integrity sha512-1EKCBSFV745UI2zn5v75sKcvVdmd+y2JtZhw8CItiQkRnBLv4l4d/RZYy+cKOuXJGsoBrKtxXn5sl7HebwQbPw==
   dependencies:
-    "@lexical/utils" "0.12.2"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
     prismjs "^1.27.0"
 
-"@lexical/dragon@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.12.2.tgz"
-  integrity sha512-Mt8NLzTOt+VgQtc2DKDbHBwKeRlvKqbLqRIMYUVk60gol+YV7NpVBsP1PAMuYYjrTQLhlckBSC32H1SUHZRavA==
+"@lexical/devtools-core@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/devtools-core/-/devtools-core-0.16.0.tgz#326c8e2995ce6e6e9e1fc4654ee2affbecdbd46d"
+  integrity sha512-Jt8p0J0UoMHf3UMh3VdyrXbLLwpEZuMqihTmbPRpwo+YQ6NGQU35QgwY2K0DpPAThpxL/Cm7uaFqGOy8Kjrhqw==
+  dependencies:
+    "@lexical/html" "0.16.0"
+    "@lexical/link" "0.16.0"
+    "@lexical/mark" "0.16.0"
+    "@lexical/table" "0.16.0"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/hashtag@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.12.2.tgz"
-  integrity sha512-2vYzIu5Ldf+eYdUrNA2m80c3N3MF3vJ0fIJzpl5QyX8OdViggEWl1bh+lKtw1Ju0H0CUyDIXdDLZ2apW3WDkTA==
+"@lexical/dragon@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.16.0.tgz#de083903701af2bb5264309b565d613c3eec06a0"
+  integrity sha512-Yr29SFZzOPs+S6UrEZaXnnso1fJGVfZOXVJQZbyzlspqJpSHXVH7InOXYHWN6JSWQ8Hs/vU3ksJXwqz+0TCp2g==
   dependencies:
-    "@lexical/utils" "0.12.2"
+    lexical "0.16.0"
 
-"@lexical/history@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/history/-/history-0.12.2.tgz"
-  integrity sha512-PM/EDjnUyBPMWh1UiYb7T+FLbvTk14HwUWLXvZxn72S6Kj8ExH/PfLbWZWLCFL8RfzvbP407VwfSN8S0bF5H6g==
+"@lexical/hashtag@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.16.0.tgz#ea0187060a114678753adaf0a15aad59d4f49a71"
+  integrity sha512-2EdAvxYVYqb0nv6vgxCRgE8ip7yez5p0y0oeUyxmdbcfZdA+Jl90gYH3VdevmZ5Bk3wE0/fIqiLD+Bb5smqjCQ==
   dependencies:
-    "@lexical/utils" "0.12.2"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/html@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/html/-/html-0.12.2.tgz"
-  integrity sha512-LWUO6OKhDtDZa9X1spHAqzsp+4EF01exis4cz5H9y2sHi7EofogXnRCadZ+fa07NVwPVTZWsStkk5qdSe/NEzg==
+"@lexical/history@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.16.0.tgz#f83f2e331957208c5c8186d98f2f84681d936cec"
+  integrity sha512-xwFxgDZGviyGEqHmgt6A6gPhsyU/yzlKRk9TBUVByba3khuTknlJ1a80H5jb+OYcrpiElml7iVuGYt+oC7atCA==
   dependencies:
-    "@lexical/selection" "0.12.2"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/link@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/link/-/link-0.12.2.tgz"
-  integrity sha512-etOIONa7uyRDmwg8GN52kDlf8thD2Zk1LOFLeocHWz1V8fe3i2unGUek5s/rNPkc6ynpPpNsHdN1VEghOLCCmw==
+"@lexical/html@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.16.0.tgz#98477ed0dee4c7d910608f4e4de3fbd5eeecdffe"
+  integrity sha512-okxn3q/1qkUpCZNEFRI39XeJj4YRjb6prm3WqZgP4d39DI1W24feeTZJjYRCW+dc3NInwFaolU3pNA2MGkjRtg==
   dependencies:
-    "@lexical/utils" "0.12.2"
+    "@lexical/selection" "0.16.0"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/list@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/list/-/list-0.12.2.tgz"
-  integrity sha512-3CyWtYQC+IlK4cK/oiD8Uz1gSXD8UcKGOF2vVsDXkMU06O6zvHNmHZOnVJqA0JVNgZAoR9dMR1fi2xd4iuCAiw==
+"@lexical/link@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.16.0.tgz#f137ab3071206ed3c3a8b8a302ed66b084399ed1"
+  integrity sha512-ppvJSh/XGqlzbeymOiwcXJcUcrqgQqTK2QXTBAZq7JThtb0WsJxYd2CSLSN+Ycu23prnwqOqILcU0+34+gAVFw==
   dependencies:
-    "@lexical/utils" "0.12.2"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/mark@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/mark/-/mark-0.12.2.tgz"
-  integrity sha512-ub+37PDfmThsqAWipRTrwqpgE+83ckqJ5C3mKQUBZvhZfVZW1rEUXZnKjFh2Q3eZK6iT7zVgoVJWJS9ZgEEyag==
+"@lexical/list@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.16.0.tgz#ed97733633492e89c68ad51a1d455b63ce5aa1c0"
+  integrity sha512-nBx/DMM7nCgnOzo1JyNnVaIrk/Xi5wIPNi8jixrEV6w9Om2K6dHutn/79Xzp2dQlNGSLHEDjky6N2RyFgmXh0g==
   dependencies:
-    "@lexical/utils" "0.12.2"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/markdown@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.12.2.tgz"
-  integrity sha512-F2jTFtBp7Q+yoA11BeUOEcxhROzW+HUhUGdsn20pSLhuxsWRj3oUuryWFeNKFofpzTCVoqU6dwpaMNMI2mL/sQ==
+"@lexical/mark@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.16.0.tgz#e87d92845c8bd231ef47106c5d44e7e10d2a3934"
+  integrity sha512-WMR4nqygSgIQ6Vdr5WAzohxBGjH+m44dBNTbWTGZGVlRvPzvBT6tieCoxFqpceIq/ko67HGTCNoFj2cMKVwgIA==
   dependencies:
-    "@lexical/code" "0.12.2"
-    "@lexical/link" "0.12.2"
-    "@lexical/list" "0.12.2"
-    "@lexical/rich-text" "0.12.2"
-    "@lexical/text" "0.12.2"
-    "@lexical/utils" "0.12.2"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/offset@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/offset/-/offset-0.12.2.tgz"
-  integrity sha512-rZLZXfOBmpmM8A2UZsX3cr/CQYw5F/ou67AbaKI0WImb5sjnIgICZqzu9VFUnkKlVNUurEpplV3UG3D1YYh1OQ==
+"@lexical/markdown@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.16.0.tgz#fd2d2759d9d5554d9899c3e1fb30a868bfa162a2"
+  integrity sha512-7HQLFrBbpY68mcq4A6C1qIGmjgA+fAByditi2WRe7tD2eoIKb/B5baQAnDKis0J+m5kTaCBmdlT6csSzyOPzeQ==
+  dependencies:
+    "@lexical/code" "0.16.0"
+    "@lexical/link" "0.16.0"
+    "@lexical/list" "0.16.0"
+    "@lexical/rich-text" "0.16.0"
+    "@lexical/text" "0.16.0"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
+
+"@lexical/offset@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.16.0.tgz#bb3bc695ed403db0795f095330c68cdc5cbbec4b"
+  integrity sha512-4TqPEC2qA7sgO8Tm65nOWnhJ8dkl22oeuGv9sUB+nhaiRZnw3R45mDelg23r56CWE8itZnvueE7TKvV+F3OXtQ==
+  dependencies:
+    lexical "0.16.0"
 
-"@lexical/overflow@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.12.2.tgz"
-  integrity sha512-UgE5j3ukO6qRFRpH4T7m/DvnodE9nCtImD7QinyGdsTa0hi5xlRnl0FUo605vH+vz7xEsUNAGwQXYPX9Sc/vig==
+"@lexical/overflow@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.16.0.tgz#31b791f7f7005ea4b160f3ae8083a2b3de05cfdc"
+  integrity sha512-a7gtIRxleEuMN9dj2yO4CdezBBfIr9Mq+m7G5z62+xy7VL7cfMfF+xWjy3EmDYDXS4vOQgAXAUgO4oKz2AKGhQ==
+  dependencies:
+    lexical "0.16.0"
 
-"@lexical/plain-text@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.12.2.tgz"
-  integrity sha512-Lcg6+ngRnX70//kz34azYhID3bvW66HSHCfu5UPhCXT+vQ/Jkd/InhRKajBwWXpaJxMM1huoi3sjzVDb3luNtw==
+"@lexical/plain-text@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.16.0.tgz#b903bfb59fb6629ded24194e1bef451df3383393"
+  integrity sha512-BK7/GSOZUHRJTbNPkpb9a/xN9z+FBCdunTsZhnOY8pQ7IKws3kuMO2Tk1zXfTd882ZNAxFdDKNdLYDSeufrKpw==
+  dependencies:
+    "@lexical/clipboard" "0.16.0"
+    "@lexical/selection" "0.16.0"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/react@^0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/react/-/react-0.12.2.tgz"
-  integrity sha512-ZBUvf5xmhiYWBw8pPrhYmLAEwFWrbF/cd15y76TUKD9l/2zDwwPs6nJQxBzfz3ei65r2/nnavLDV8W3QfvxfUA==
-  dependencies:
-    "@lexical/clipboard" "0.12.2"
-    "@lexical/code" "0.12.2"
-    "@lexical/dragon" "0.12.2"
-    "@lexical/hashtag" "0.12.2"
-    "@lexical/history" "0.12.2"
-    "@lexical/link" "0.12.2"
-    "@lexical/list" "0.12.2"
-    "@lexical/mark" "0.12.2"
-    "@lexical/markdown" "0.12.2"
-    "@lexical/overflow" "0.12.2"
-    "@lexical/plain-text" "0.12.2"
-    "@lexical/rich-text" "0.12.2"
-    "@lexical/selection" "0.12.2"
-    "@lexical/table" "0.12.2"
-    "@lexical/text" "0.12.2"
-    "@lexical/utils" "0.12.2"
-    "@lexical/yjs" "0.12.2"
+"@lexical/react@^0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.16.0.tgz#0bd3ae63ceb5ad8b77e8c0e8ba7df1a0369462f0"
+  integrity sha512-WKFQbI0/m1YkLjL5t90YLJwjGcl5QRe6mkfm3ljQuL7Ioj3F92ZN/J2gHFVJ9iC8/lJs6Zzw6oFjiP8hQxJf9Q==
+  dependencies:
+    "@lexical/clipboard" "0.16.0"
+    "@lexical/code" "0.16.0"
+    "@lexical/devtools-core" "0.16.0"
+    "@lexical/dragon" "0.16.0"
+    "@lexical/hashtag" "0.16.0"
+    "@lexical/history" "0.16.0"
+    "@lexical/link" "0.16.0"
+    "@lexical/list" "0.16.0"
+    "@lexical/mark" "0.16.0"
+    "@lexical/markdown" "0.16.0"
+    "@lexical/overflow" "0.16.0"
+    "@lexical/plain-text" "0.16.0"
+    "@lexical/rich-text" "0.16.0"
+    "@lexical/selection" "0.16.0"
+    "@lexical/table" "0.16.0"
+    "@lexical/text" "0.16.0"
+    "@lexical/utils" "0.16.0"
+    "@lexical/yjs" "0.16.0"
+    lexical "0.16.0"
     react-error-boundary "^3.1.4"
 
-"@lexical/rich-text@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.12.2.tgz"
-  integrity sha512-igsEuv7CwBOAj5c8jeE41cnx6zkhI/Bkbu4W7shT6S6lNA/3cnyZpAMlgixwyK5RoqjGRCT+IJK5l6yBxQfNkw==
+"@lexical/rich-text@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.16.0.tgz#5b9ea6ceb1ea034fa7adf1770bd7fa6af1571d1d"
+  integrity sha512-AGTD6yJZ+kj2TNah1r7/6vyufs6fZANeSvv9x5eG+WjV4uyUJYkd1qR8C5gFZHdkyr+bhAcsAXvS039VzAxRrQ==
+  dependencies:
+    "@lexical/clipboard" "0.16.0"
+    "@lexical/selection" "0.16.0"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/selection@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/selection/-/selection-0.12.2.tgz"
-  integrity sha512-h+g3oOnihHKIyLTyG6uLCEVR/DmUEVdCcZO1iAoGsuW7nwWiWNPWj6oZ3Cw5J1Mk5u62DHnkkVDQsVSZbAwmtg==
+"@lexical/selection@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.16.0.tgz#8e09edb1e555e79c646a0105beab58ac21fc7158"
+  integrity sha512-trT9gQVJ2j6AwAe7tHJ30SRuxCpV6yR9LFtggxphHsXSvJYnoHC0CXh1TF2jHl8Gd5OsdWseexGLBE4Y0V3gwQ==
+  dependencies:
+    lexical "0.16.0"
 
-"@lexical/table@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/table/-/table-0.12.2.tgz"
-  integrity sha512-tiAmTq6RKHDVER9v589Ajm9/RL+WTF1WschrH6HHVCtil6cfJfTJeJ+MF45+XEzB9fkqy2LfrScAfWxqLjVePA==
+"@lexical/table@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.16.0.tgz#68592afbb0f9c0d9bf42bebaae626b8129fc470d"
+  integrity sha512-A66K779kxdr0yH2RwT2itsMnkzyFLFNPXyiWGLobCH8ON4QPuBouZvjbRHBe8Pe64yJ0c1bRDxSbTqUi9Wt3Gg==
   dependencies:
-    "@lexical/utils" "0.12.2"
+    "@lexical/utils" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/text@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/text/-/text-0.12.2.tgz"
-  integrity sha512-HyuIGuQvVi5djJKKBf+jYEBjK+0Eo9cKHf6WS7dlFozuCZvcCQEJkFy2yceWOwIVk+f2kptVQ5uO7aiZHExH2A==
+"@lexical/text@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.16.0.tgz#fc4789591f8aaa4a33bc1814280bc8725fd036a9"
+  integrity sha512-9ilaOhuNIIGHKC8g8j3K/mEvJ09af9B6RKbm3GNoRcf/WNHD4dEFWNTEvgo/3zCzAS8EUBI6UINmfQQWlMjdIQ==
+  dependencies:
+    lexical "0.16.0"
 
-"@lexical/utils@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/utils/-/utils-0.12.2.tgz"
-  integrity sha512-xW4y4l2Yd37+qLwkBvBGyzsKCA9wnh1ljphBJeR2vreT193i2gaIwuku2ZKlER14VHw4192qNJF7vUoAEmwurQ==
+"@lexical/utils@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.16.0.tgz#6ad5785c53347aed5b39c980240c09b21c4a7469"
+  integrity sha512-GWmFEmd7o3GHqJBaEwzuZQbfTNI3Gg8ReGuHMHABgrkhZ8j2NggoRBlxsQLG0f7BewfTMVwbye22yBPq78775w==
   dependencies:
-    "@lexical/list" "0.12.2"
-    "@lexical/selection" "0.12.2"
-    "@lexical/table" "0.12.2"
+    "@lexical/list" "0.16.0"
+    "@lexical/selection" "0.16.0"
+    "@lexical/table" "0.16.0"
+    lexical "0.16.0"
 
-"@lexical/yjs@0.12.2":
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.12.2.tgz"
-  integrity sha512-OPJhkJD1Mp9W80mfLzASTB3OFWFMzJteUYA+eSyDgiX9zNi1VGxAqmIITTkDvnCMa+qvw4EfhGeGezpjx6Og4A==
+"@lexical/yjs@0.16.0":
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.16.0.tgz#e27bec25c12e90f7768b980da08f2d2d9919d25b"
+  integrity sha512-YIJr87DfAXTwoVHDjR7cci//hr4r/a61Nn95eo2JNwbTqQo65Gp8rwJivqVxNfvKZmRdwHTKgvdEDoBmI/tGog==
   dependencies:
-    "@lexical/offset" "0.12.2"
+    "@lexical/offset" "0.16.0"
+    lexical "0.16.0"
 
 "@mdx-js/loader@^2.3.0":
   version "2.3.0"
@@ -4287,10 +4334,10 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
-lexical@^0.12.2:
-  version "0.12.2"
-  resolved "https://registry.npmjs.org/lexical/-/lexical-0.12.2.tgz"
-  integrity sha512-Kxavd+ETjxtVwG/hvPd6WZfXD44sLOKe9Vlkwxy7lBQ1qZArS+rZfs+u5iXwXe6tX9f2PIM0u3RHsrCEDDE0fw==
+lexical@0.16.0, lexical@^0.16.0:
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.16.0.tgz#0515d4003cbfba5a5e0e3e50f32f65076a6b89e2"
+  integrity sha512-Skn45Qhriazq4fpAtwnAB11U//GKc4vjzx54xsV3TkDLDvWpbL4Z9TNRwRoN3g7w8AkWnqjeOSODKkrjgfRSrg==
 
 lilconfig@2.1.0, lilconfig@^2.0.5, lilconfig@^2.1.0:
   version "2.1.0"