浏览代码

feat: workflow add note node (#5164)

zxhlyh 10 月之前
父节点
当前提交
c28d709d7f
共有 69 个文件被更改,包括 2370 次插入164 次删除
  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. 二进制
      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


+ 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


文件差异内容过多而无法显示
+ 5 - 0
web/app/components/base/icons/assets/vender/line/files/sticker-square.svg


二进制
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


+ 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


+ 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


+ 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()
@@ -80,6 +82,16 @@ const PanelContextmenu = () => {
         />
         <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={() => {
             handleStartWorkflowRun()
             handlePaneContextmenuCancel()

+ 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"