Преглед изворни кода

feat: llm support jinja fe (#4260)

Joel пре 11 месеци
родитељ
комит
01555463d2
21 измењених фајлова са 621 додато и 177 уклоњено
  1. 13 0
      web/app/components/base/icons/assets/vender/workflow/jinja.svg
  2. 98 0
      web/app/components/base/icons/src/vender/workflow/Jinja.json
  3. 16 0
      web/app/components/base/icons/src/vender/workflow/Jinja.tsx
  4. 1 0
      web/app/components/base/icons/src/vender/workflow/index.ts
  5. 4 1
      web/app/components/base/switch/index.tsx
  6. 50 7
      web/app/components/base/tooltip-plus/index.tsx
  7. 9 13
      web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx
  8. 100 40
      web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
  9. 4 0
      web/app/components/workflow/nodes/_base/components/editor/code-editor/style.css
  10. 114 69
      web/app/components/workflow/nodes/_base/components/prompt/editor.tsx
  11. 15 2
      web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx
  12. 34 6
      web/app/components/workflow/nodes/llm/components/config-prompt.tsx
  13. 19 4
      web/app/components/workflow/nodes/llm/default.ts
  14. 27 0
      web/app/components/workflow/nodes/llm/panel.tsx
  15. 3 1
      web/app/components/workflow/nodes/llm/types.ts
  16. 94 33
      web/app/components/workflow/nodes/llm/use-config.ts
  17. 2 1
      web/app/components/workflow/nodes/template-transform/panel.tsx
  18. 7 0
      web/app/components/workflow/nodes/template-transform/use-config.ts
  19. 7 0
      web/app/components/workflow/types.ts
  20. 2 0
      web/i18n/en-US/workflow.ts
  21. 2 0
      web/i18n/zh-Hans/workflow.ts

+ 13 - 0
web/app/components/base/icons/assets/vender/workflow/jinja.svg

@@ -0,0 +1,13 @@
+<svg width="24" height="12" viewBox="0 0 24 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Jinja Icon">
+<g id="Vector">
+<path d="M7.46013 5.99982C7.46013 4.87982 7.48013 3.92982 7.53013 3.16982V3.06982L6.13013 3.23982L6.15013 3.32982C6.29013 4.03982 6.36013 4.93982 6.36013 5.99982C6.36013 6.93982 6.33013 7.78982 6.28013 8.51982V8.60982H7.55013V8.51982C7.49013 7.72982 7.46013 6.87982 7.46013 5.99982Z" fill="#667085"/>
+<path d="M3.33016 1.31998C3.38016 2.31998 3.38016 5.13998 3.38016 7.00998V7.77998C3.38016 8.21998 3.35016 8.58998 3.28016 8.85998C3.22016 9.12998 3.11016 9.34998 2.96016 9.52998C2.82016 9.70998 2.62016 9.83998 2.37016 9.92998C2.12016 10.01 1.82016 10.06 1.49016 10.06C1.19016 10.06 0.900156 9.99998 0.620156 9.87998L0.520156 9.83998L0.410156 10.83L0.480156 10.85C0.800156 10.93 1.16016 10.97 1.56016 10.97C2.08016 10.97 2.53016 10.9 2.90016 10.77C3.28016 10.64 3.59016 10.43 3.83016 10.15C4.07016 9.87998 4.25016 9.52998 4.36016 9.13998C4.47016 8.74998 4.53016 8.23998 4.53016 7.64998C4.53016 6.78998 4.59016 3.54998 4.59016 3.17998C4.61016 2.47998 4.63016 1.86998 4.66016 1.31998V1.22998H3.33016V1.31998Z" fill="#667085"/>
+<path d="M7.08021 0.919922C6.82022 0.919922 6.60021 0.999922 6.45021 1.14992C6.30021 1.29992 6.22021 1.47992 6.22021 1.68992C6.22021 1.87992 6.28021 2.04992 6.41021 2.18992C6.54022 2.31992 6.73022 2.38992 6.96022 2.38992C7.23022 2.38992 7.44021 2.30992 7.59021 2.15992C7.74021 1.99992 7.81021 1.81992 7.81021 1.60992C7.81021 1.42992 7.74021 1.25992 7.61021 1.12992C7.48021 0.989922 7.30021 0.919922 7.08021 0.919922Z" fill="#667085"/>
+<path d="M15.6102 3.30981C15.7702 4.07981 15.8502 5.25981 15.8502 6.81981C15.8502 8.26981 15.7902 9.23981 15.6702 9.67981C15.5902 9.96981 15.3802 10.2598 15.0302 10.5198L14.9702 10.5698L15.3502 11.0998H15.4002C16.4302 10.8198 16.9602 10.0598 16.9602 8.83981C16.9602 8.64981 16.9502 8.30981 16.9202 7.80981C16.9002 7.31981 16.8902 6.90981 16.8902 6.59981C16.8902 5.44981 16.9202 4.28981 16.9902 3.15981V3.05981L15.5802 3.21981L15.6002 3.30981H15.6102Z" fill="#667085"/>
+<path d="M14.2901 5.77C14.2901 5.7 14.2901 5.56 14.3001 5.36C14.3001 5.15 14.3101 5.01 14.3101 4.94C14.3101 4.22 14.1101 3.71 13.7201 3.43C13.3401 3.15 12.8001 3 12.1101 3C11.4201 3 10.7901 3.24 10.2001 3.71L10.0901 3.06L8.8501 3.22L8.8701 3.31C9.0501 4.11 9.1401 4.95 9.1401 5.8C9.1401 6.36 9.1101 7.27 9.0401 8.52V8.61H10.3101V8.53C10.2901 7.07 10.2801 5.71 10.2801 4.49C10.7401 4.14 11.2501 3.96 11.7901 3.96C12.2401 3.96 12.5801 4.06 12.8201 4.26C13.0501 4.45 13.1701 4.82 13.1701 5.36C13.1701 6.5 13.1301 7.56 13.0401 8.53V8.62H14.3101V8.54C14.2901 7.35 14.2801 6.42 14.2801 5.79L14.2901 5.77Z" fill="#667085"/>
+<path d="M16.5302 0.919922C16.2702 0.919922 16.0502 0.999922 15.9002 1.14992C15.7502 1.29992 15.6702 1.47992 15.6702 1.68992C15.6702 1.87992 15.7302 2.04992 15.8602 2.18992C15.9902 2.31992 16.1802 2.38992 16.4102 2.38992C16.6702 2.38992 16.8902 2.30992 17.0302 2.15992C17.1802 1.99992 17.2502 1.81992 17.2502 1.60992C17.2502 1.42992 17.1802 1.25992 17.0502 1.12992C16.9202 0.989922 16.7402 0.919922 16.5202 0.919922H16.5302Z" fill="#667085"/>
+<path d="M23.1802 8.51001C23.0702 8.00001 23.0202 7.40001 23.0202 6.73001C23.0202 6.57001 23.0202 6.26001 23.0402 5.83001C23.0602 5.38001 23.0702 5.06001 23.0702 4.88001C23.0702 4.20001 22.8602 3.71001 22.4502 3.43001C22.0402 3.15001 21.4702 3.01001 20.7302 3.01001C19.9402 3.01001 19.2302 3.09001 18.6102 3.25001H18.5602L18.4302 4.20001L18.5502 4.17001C19.1602 4.03001 19.7802 3.96001 20.4102 3.96001C20.9302 3.96001 21.3202 4.03001 21.5702 4.18001C21.8102 4.31001 21.9302 4.59001 21.9302 5.01001C21.9302 5.09001 21.9302 5.16001 21.9302 5.23001C20.5102 5.25001 19.5602 5.44001 19.0302 5.79001C18.4802 6.15001 18.2002 6.63001 18.2002 7.23001C18.2002 7.72001 18.3802 8.10001 18.7402 8.36001C19.0902 8.62001 19.5102 8.75001 19.9902 8.75001C20.8202 8.75001 21.5002 8.55001 22.0102 8.17001C22.0102 8.30001 22.0402 8.44001 22.0802 8.58001L22.1002 8.64001L23.2202 8.60001L23.2002 8.50001L23.1802 8.51001ZM20.2802 6.18001C20.6502 6.08001 21.2002 6.03001 21.9102 6.03001C21.9102 6.45001 21.9202 6.92001 21.9402 7.42001C21.5602 7.69001 21.0502 7.83001 20.4302 7.83001C19.7002 7.83001 19.3502 7.61001 19.3502 7.16001C19.3502 6.68001 19.6602 6.36001 20.2802 6.18001Z" fill="#667085"/>
+</g>
+</g>
+</svg>

+ 98 - 0
web/app/components/base/icons/src/vender/workflow/Jinja.json

@@ -0,0 +1,98 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "24",
+			"height": "12",
+			"viewBox": "0 0 24 12",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Jinja Icon"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Vector"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M7.46013 5.99982C7.46013 4.87982 7.48013 3.92982 7.53013 3.16982V3.06982L6.13013 3.23982L6.15013 3.32982C6.29013 4.03982 6.36013 4.93982 6.36013 5.99982C6.36013 6.93982 6.33013 7.78982 6.28013 8.51982V8.60982H7.55013V8.51982C7.49013 7.72982 7.46013 6.87982 7.46013 5.99982Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M3.33016 1.31998C3.38016 2.31998 3.38016 5.13998 3.38016 7.00998V7.77998C3.38016 8.21998 3.35016 8.58998 3.28016 8.85998C3.22016 9.12998 3.11016 9.34998 2.96016 9.52998C2.82016 9.70998 2.62016 9.83998 2.37016 9.92998C2.12016 10.01 1.82016 10.06 1.49016 10.06C1.19016 10.06 0.900156 9.99998 0.620156 9.87998L0.520156 9.83998L0.410156 10.83L0.480156 10.85C0.800156 10.93 1.16016 10.97 1.56016 10.97C2.08016 10.97 2.53016 10.9 2.90016 10.77C3.28016 10.64 3.59016 10.43 3.83016 10.15C4.07016 9.87998 4.25016 9.52998 4.36016 9.13998C4.47016 8.74998 4.53016 8.23998 4.53016 7.64998C4.53016 6.78998 4.59016 3.54998 4.59016 3.17998C4.61016 2.47998 4.63016 1.86998 4.66016 1.31998V1.22998H3.33016V1.31998Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M7.08021 0.919922C6.82022 0.919922 6.60021 0.999922 6.45021 1.14992C6.30021 1.29992 6.22021 1.47992 6.22021 1.68992C6.22021 1.87992 6.28021 2.04992 6.41021 2.18992C6.54022 2.31992 6.73022 2.38992 6.96022 2.38992C7.23022 2.38992 7.44021 2.30992 7.59021 2.15992C7.74021 1.99992 7.81021 1.81992 7.81021 1.60992C7.81021 1.42992 7.74021 1.25992 7.61021 1.12992C7.48021 0.989922 7.30021 0.919922 7.08021 0.919922Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M15.6102 3.30981C15.7702 4.07981 15.8502 5.25981 15.8502 6.81981C15.8502 8.26981 15.7902 9.23981 15.6702 9.67981C15.5902 9.96981 15.3802 10.2598 15.0302 10.5198L14.9702 10.5698L15.3502 11.0998H15.4002C16.4302 10.8198 16.9602 10.0598 16.9602 8.83981C16.9602 8.64981 16.9502 8.30981 16.9202 7.80981C16.9002 7.31981 16.8902 6.90981 16.8902 6.59981C16.8902 5.44981 16.9202 4.28981 16.9902 3.15981V3.05981L15.5802 3.21981L15.6002 3.30981H15.6102Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M14.2901 5.77C14.2901 5.7 14.2901 5.56 14.3001 5.36C14.3001 5.15 14.3101 5.01 14.3101 4.94C14.3101 4.22 14.1101 3.71 13.7201 3.43C13.3401 3.15 12.8001 3 12.1101 3C11.4201 3 10.7901 3.24 10.2001 3.71L10.0901 3.06L8.8501 3.22L8.8701 3.31C9.0501 4.11 9.1401 4.95 9.1401 5.8C9.1401 6.36 9.1101 7.27 9.0401 8.52V8.61H10.3101V8.53C10.2901 7.07 10.2801 5.71 10.2801 4.49C10.7401 4.14 11.2501 3.96 11.7901 3.96C12.2401 3.96 12.5801 4.06 12.8201 4.26C13.0501 4.45 13.1701 4.82 13.1701 5.36C13.1701 6.5 13.1301 7.56 13.0401 8.53V8.62H14.3101V8.54C14.2901 7.35 14.2801 6.42 14.2801 5.79L14.2901 5.77Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M16.5302 0.919922C16.2702 0.919922 16.0502 0.999922 15.9002 1.14992C15.7502 1.29992 15.6702 1.47992 15.6702 1.68992C15.6702 1.87992 15.7302 2.04992 15.8602 2.18992C15.9902 2.31992 16.1802 2.38992 16.4102 2.38992C16.6702 2.38992 16.8902 2.30992 17.0302 2.15992C17.1802 1.99992 17.2502 1.81992 17.2502 1.60992C17.2502 1.42992 17.1802 1.25992 17.0502 1.12992C16.9202 0.989922 16.7402 0.919922 16.5202 0.919922H16.5302Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M23.1802 8.51001C23.0702 8.00001 23.0202 7.40001 23.0202 6.73001C23.0202 6.57001 23.0202 6.26001 23.0402 5.83001C23.0602 5.38001 23.0702 5.06001 23.0702 4.88001C23.0702 4.20001 22.8602 3.71001 22.4502 3.43001C22.0402 3.15001 21.4702 3.01001 20.7302 3.01001C19.9402 3.01001 19.2302 3.09001 18.6102 3.25001H18.5602L18.4302 4.20001L18.5502 4.17001C19.1602 4.03001 19.7802 3.96001 20.4102 3.96001C20.9302 3.96001 21.3202 4.03001 21.5702 4.18001C21.8102 4.31001 21.9302 4.59001 21.9302 5.01001C21.9302 5.09001 21.9302 5.16001 21.9302 5.23001C20.5102 5.25001 19.5602 5.44001 19.0302 5.79001C18.4802 6.15001 18.2002 6.63001 18.2002 7.23001C18.2002 7.72001 18.3802 8.10001 18.7402 8.36001C19.0902 8.62001 19.5102 8.75001 19.9902 8.75001C20.8202 8.75001 21.5002 8.55001 22.0102 8.17001C22.0102 8.30001 22.0402 8.44001 22.0802 8.58001L22.1002 8.64001L23.2202 8.60001L23.2002 8.50001L23.1802 8.51001ZM20.2802 6.18001C20.6502 6.08001 21.2002 6.03001 21.9102 6.03001C21.9102 6.45001 21.9202 6.92001 21.9402 7.42001C21.5602 7.69001 21.0502 7.83001 20.4302 7.83001C19.7002 7.83001 19.3502 7.61001 19.3502 7.16001C19.3502 6.68001 19.6602 6.36001 20.2802 6.18001Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Jinja"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/workflow/Jinja.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Jinja.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 = 'Jinja'
+
+export default Icon

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

@@ -4,6 +4,7 @@ export { default as End } from './End'
 export { default as Home } from './Home'
 export { default as Http } from './Http'
 export { default as IfElse } from './IfElse'
+export { default as Jinja } from './Jinja'
 export { default as KnowledgeRetrieval } from './KnowledgeRetrieval'
 export { default as Llm } from './Llm'
 export { default as QuestionClassifier } from './QuestionClassifier'

+ 4 - 1
web/app/components/base/switch/index.tsx

@@ -5,7 +5,7 @@ import { Switch as OriginalSwitch } from '@headlessui/react'
 
 type SwitchProps = {
   onChange: (value: boolean) => void
-  size?: 'md' | 'lg' | 'l'
+  size?: 'sm' | 'md' | 'lg' | 'l'
   defaultValue?: boolean
   disabled?: boolean
 }
@@ -19,18 +19,21 @@ const Switch = ({ onChange, size = 'lg', defaultValue = false, disabled = false
     lg: 'h-6 w-11',
     l: 'h-5 w-9',
     md: 'h-4 w-7',
+    sm: 'h-3 w-5',
   }
 
   const circleStyle = {
     lg: 'h-5 w-5',
     l: 'h-4 w-4',
     md: 'h-3 w-3',
+    sm: 'h-2 w-2',
   }
 
   const translateLeft = {
     lg: 'translate-x-5',
     l: 'translate-x-4',
     md: 'translate-x-3',
+    sm: 'translate-x-2',
   }
   return (
     <OriginalSwitch

+ 50 - 7
web/app/components/base/tooltip-plus/index.tsx

@@ -1,7 +1,8 @@
 'use client'
 import type { FC } from 'react'
-import React, { useState } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
 import cn from 'classnames'
+import { useBoolean } from 'ahooks'
 import type { OffsetOptions, Placement } from '@floating-ui/react'
 import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
 export type TooltipProps = {
@@ -28,6 +29,39 @@ const Tooltip: FC<TooltipProps> = ({
   offset,
 }) => {
   const [open, setOpen] = useState(false)
+  const [isHoverPopup, {
+    setTrue: setHoverPopup,
+    setFalse: setNotHoverPopup,
+  }] = useBoolean(false)
+
+  const isHoverPopupRef = useRef(isHoverPopup)
+  useEffect(() => {
+    isHoverPopupRef.current = isHoverPopup
+  }, [isHoverPopup])
+
+  const [isHoverTrigger, {
+    setTrue: setHoverTrigger,
+    setFalse: setNotHoverTrigger,
+  }] = useBoolean(false)
+
+  const isHoverTriggerRef = useRef(isHoverTrigger)
+  useEffect(() => {
+    isHoverTriggerRef.current = isHoverTrigger
+  }, [isHoverTrigger])
+
+  const handleLeave = (isTrigger: boolean) => {
+    if (isTrigger)
+      setNotHoverTrigger()
+
+    else
+      setNotHoverPopup()
+
+    // give time to move to the popup
+    setTimeout(() => {
+      if (!isHoverPopupRef.current && !isHoverTriggerRef.current)
+        setOpen(false)
+    }, 500)
+  }
 
   return (
     <PortalToFollowElem
@@ -38,18 +72,27 @@ const Tooltip: FC<TooltipProps> = ({
     >
       <PortalToFollowElemTrigger
         onClick={() => triggerMethod === 'click' && setOpen(v => !v)}
-        onMouseEnter={() => triggerMethod === 'hover' && setOpen(true)}
-        onMouseLeave={() => triggerMethod === 'hover' && setOpen(false)}
+        onMouseEnter={() => {
+          if (triggerMethod === 'hover') {
+            setHoverTrigger()
+            setOpen(true)
+          }
+        }}
+        onMouseLeave={() => triggerMethod === 'hover' && handleLeave(true)}
       >
         {children}
       </PortalToFollowElemTrigger>
       <PortalToFollowElemContent
         className="z-[9999]"
       >
-        <div className={cn(
-          'relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg',
-          popupClassName,
-        )}>
+        <div
+          className={cn(
+            'relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg',
+            popupClassName,
+          )}
+          onMouseEnter={() => triggerMethod === 'hover' && setHoverPopup()}
+          onMouseLeave={() => triggerMethod === 'hover' && handleLeave(false)}
+        >
           {popupContent}
           {!hideArrow && arrow}
         </div>

+ 9 - 13
web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx

@@ -3,33 +3,28 @@ import type { FC } from 'react'
 import React, { useEffect, useRef, useState } from 'react'
 import { useBoolean } from 'ahooks'
 import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
 import type { Props as EditorProps } from '.'
 import Editor from '.'
 import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
-import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
-import type { Variable } from '@/app/components/workflow/types'
+import type { NodeOutPutVar, Variable } from '@/app/components/workflow/types'
 
 const TO_WINDOW_OFFSET = 8
 
 type Props = {
-  nodeId: string
+  availableVars: NodeOutPutVar[]
   varList: Variable[]
-  onAddVar: (payload: Variable) => void
+  onAddVar?: (payload: Variable) => void
 } & EditorProps
 
 const CodeEditor: FC<Props> = ({
-  nodeId,
+  availableVars,
   varList,
   onAddVar,
   ...editorProps
 }) => {
   const { t } = useTranslation()
 
-  const { availableVars } = useAvailableVarList(nodeId, {
-    onlyLeafNodeVar: false,
-    filterVar: () => true,
-  })
-
   const isLeftBraceRef = useRef(false)
 
   const editorRef = useRef(null)
@@ -76,7 +71,8 @@ const CodeEditor: FC<Props> = ({
       if (popupPosition.y + height > window.innerHeight - TO_WINDOW_OFFSET)
         newPopupPosition.y = window.innerHeight - height - TO_WINDOW_OFFSET
 
-      setPopupPosition(newPopupPosition)
+      if (newPopupPosition.x !== popupPosition.x || newPopupPosition.y !== popupPosition.y)
+        setPopupPosition(newPopupPosition)
     }
   }, [isShowVarPicker, popupPosition])
 
@@ -124,7 +120,7 @@ const CodeEditor: FC<Props> = ({
         value_selector: varValue,
       }
 
-      onAddVar(newVar)
+      onAddVar?.(newVar)
     }
     const editor: any = editorRef.current
     const monaco: any = monacoRef.current
@@ -143,7 +139,7 @@ const CodeEditor: FC<Props> = ({
   }
 
   return (
-    <div>
+    <div className={cn(editorProps.isExpand && 'h-full')}>
       <Editor
         {...editorProps}
         onMount={onEditorMounted}

+ 100 - 40
web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx

@@ -1,20 +1,23 @@
 'use client'
 import type { FC } from 'react'
 import Editor, { loader } from '@monaco-editor/react'
-
-import React, { useRef } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
+import cn from 'classnames'
 import Base from '../base'
 import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
+
 import './style.css'
 
 // load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
 loader.config({ paths: { vs: '/vs' } })
 
+const CODE_EDITOR_LINE_HEIGHT = 18
+
 export type Props = {
   value?: string | object
   placeholder?: string
   onChange?: (value: string) => void
-  title: JSX.Element
+  title?: JSX.Element
   language: CodeLanguage
   headerRight?: JSX.Element
   readOnly?: boolean
@@ -22,6 +25,8 @@ export type Props = {
   height?: number
   isInNode?: boolean
   onMount?: (editor: any, monaco: any) => void
+  noWrapper?: boolean
+  isExpand?: boolean
 }
 
 const languageMap = {
@@ -30,11 +35,20 @@ const languageMap = {
   [CodeLanguage.json]: 'json',
 }
 
+const DEFAULT_THEME = {
+  base: 'vs',
+  inherit: true,
+  rules: [],
+  colors: {
+    'editor.background': '#F2F4F7', // #00000000 transparent. But it will has a blue border
+  },
+}
+
 const CodeEditor: FC<Props> = ({
   value = '',
   placeholder = '',
   onChange = () => { },
-  title,
+  title = '',
   headerRight,
   language,
   readOnly,
@@ -42,16 +56,37 @@ const CodeEditor: FC<Props> = ({
   height,
   isInNode,
   onMount,
+  noWrapper,
+  isExpand,
 }) => {
   const [isFocus, setIsFocus] = React.useState(false)
+  const [isMounted, setIsMounted] = React.useState(false)
+  const minHeight = height || 200
+  const [editorContentHeight, setEditorContentHeight] = useState(56)
+
+  const valueRef = useRef(value)
+  useEffect(() => {
+    valueRef.current = value
+  }, [value])
+
+  const editorRef = useRef<any>(null)
+  const resizeEditorToContent = () => {
+    if (editorRef.current) {
+      const contentHeight = editorRef.current.getContentHeight() // Math.max(, minHeight)
+      setEditorContentHeight(contentHeight)
+    }
+  }
 
   const handleEditorChange = (value: string | undefined) => {
     onChange(value || '')
+    setTimeout(() => {
+      resizeEditorToContent()
+    }, 10)
   }
 
-  const editorRef = useRef(null)
   const handleEditorDidMount = (editor: any, monaco: any) => {
     editorRef.current = editor
+    resizeEditorToContent()
 
     editor.onDidFocusEditorText(() => {
       setIsFocus(true)
@@ -60,6 +95,8 @@ const CodeEditor: FC<Props> = ({
       setIsFocus(false)
     })
 
+    monaco.editor.defineTheme('default-theme', DEFAULT_THEME)
+
     monaco.editor.defineTheme('blur-theme', {
       base: 'vs',
       inherit: true,
@@ -78,7 +115,10 @@ const CodeEditor: FC<Props> = ({
       },
     })
 
+    monaco.editor.setTheme('default-theme') // Fix: sometimes not load the default theme
+
     onMount?.(editor, monaco)
+    setIsMounted(true)
   }
 
   const outPutValue = (() => {
@@ -92,43 +132,63 @@ const CodeEditor: FC<Props> = ({
     }
   })()
 
-  return (
-    <div>
-      <Base
-        className='relative'
-        title={title}
+  const theme = (() => {
+    if (noWrapper)
+      return 'default-theme'
+
+    return isFocus ? 'focus-theme' : 'blur-theme'
+  })()
+
+  const main = (
+    <>
+      {/* https://www.npmjs.com/package/@monaco-editor/react */}
+      <Editor
+        // className='min-h-[100%]' // h-full
+        // language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
+        language={languageMap[language] || 'javascript'}
+        theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme
         value={outPutValue}
-        headerRight={headerRight}
-        isFocus={isFocus && !readOnly}
-        minHeight={height || 200}
-        isInNode={isInNode}
-      >
-        <>
-          {/* https://www.npmjs.com/package/@monaco-editor/react */}
-          <Editor
-            className='h-full'
-            // language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
-            language={languageMap[language] || 'javascript'}
-            theme={isFocus ? 'focus-theme' : 'blur-theme'}
+        onChange={handleEditorChange}
+        // https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
+        options={{
+          readOnly,
+          domReadOnly: true,
+          quickSuggestions: false,
+          minimap: { enabled: false },
+          lineNumbersMinChars: 1, // would change line num width
+          wordWrap: 'on', // auto line wrap
+          // lineNumbers: (num) => {
+          //   return <div>{num}</div>
+          // }
+        }}
+        onMount={handleEditorDidMount}
+      />
+      {!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
+    </>
+  )
+
+  return (
+    <div className={cn(isExpand && 'h-full')}>
+      {noWrapper
+        ? <div className='relative no-wrapper' style={{
+          height: isExpand ? '100%' : (editorContentHeight) / 2 + CODE_EDITOR_LINE_HEIGHT, // In IDE, the last line can always be in lop line. So there is some blank space in the bottom.
+          minHeight: CODE_EDITOR_LINE_HEIGHT,
+        }}>
+          {main}
+        </div>
+        : (
+          <Base
+            className='relative'
+            title={title}
             value={outPutValue}
-            onChange={handleEditorChange}
-            // https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
-            options={{
-              readOnly,
-              domReadOnly: true,
-              quickSuggestions: false,
-              minimap: { enabled: false },
-              lineNumbersMinChars: 1, // would change line num width
-              wordWrap: 'on', // auto line wrap
-              // lineNumbers: (num) => {
-              //   return <div>{num}</div>
-              // }
-            }}
-            onMount={handleEditorDidMount}
-          />
-          {!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
-        </>
-      </Base>
+            headerRight={headerRight}
+            isFocus={isFocus && !readOnly}
+            minHeight={minHeight}
+            isInNode={isInNode}
+          >
+            {main}
+          </Base>
+        )}
     </div>
   )
 }

+ 4 - 0
web/app/components/workflow/nodes/_base/components/editor/code-editor/style.css

@@ -2,6 +2,10 @@
   padding-left: 10px;
 }
 
+.no-wrapper .margin-view-overlays {
+  padding-left: 0;
+}
+
 /* hide readonly tooltip */
 .monaco-editor-overlaymessage {
   display: none !important;

+ 114 - 69
web/app/components/workflow/nodes/_base/components/prompt/editor.tsx

@@ -1,16 +1,19 @@
 'use client'
 import type { FC } from 'react'
-import React, { useCallback, useEffect, useRef, useState } from 'react'
+import React, { useCallback, useRef } from 'react'
 import cn from 'classnames'
 import copy from 'copy-to-clipboard'
 import { useTranslation } from 'react-i18next'
 import { useBoolean } from 'ahooks'
-import {
-  BlockEnum,
-  type Node,
-  type NodeOutPutVar,
+import { BlockEnum, EditionType } from '../../../../types'
+import type {
+  Node,
+  NodeOutPutVar,
+  Variable,
 } from '../../../../types'
+
 import Wrap from '../editor/wrap'
+import { CodeLanguage } from '../../../code/types'
 import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
 import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
 import PromptEditor from '@/app/components/base/prompt-editor'
@@ -21,6 +24,10 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
 import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
 import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
 import TooltipPlus from '@/app/components/base/tooltip-plus'
+import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars'
+import Switch from '@/app/components/base/switch'
+import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
+
 type Props = {
   className?: string
   headerClassName?: string
@@ -42,6 +49,12 @@ type Props = {
   }
   nodesOutputVars?: NodeOutPutVar[]
   availableNodes?: Node[]
+  // for jinja
+  isSupportJinja?: boolean
+  editionType?: EditionType
+  onEditionTypeChange?: (editionType: EditionType) => void
+  varList?: Variable[]
+  handleAddVariable?: (payload: any) => void
 }
 
 const Editor: FC<Props> = ({
@@ -61,6 +74,11 @@ const Editor: FC<Props> = ({
   hasSetBlockStatus,
   nodesOutputVars,
   availableNodes = [],
+  isSupportJinja,
+  editionType,
+  onEditionTypeChange,
+  varList = [],
+  handleAddVariable,
 }) => {
   const { t } = useTranslation()
   const { eventEmitter } = useEventEmitterContextContext()
@@ -85,20 +103,6 @@ const Editor: FC<Props> = ({
     setTrue: setFocus,
     setFalse: setBlur,
   }] = useBoolean(false)
-  const hideTooltipRunId = useRef(0)
-
-  const [isShowInsertToolTip, setIsShowInsertTooltip] = useState(false)
-  useEffect(() => {
-    if (isFocus) {
-      clearTimeout(hideTooltipRunId.current)
-      setIsShowInsertTooltip(true)
-    }
-    else {
-      hideTooltipRunId.current = setTimeout(() => {
-        setIsShowInsertTooltip(false)
-      }, 100) as any
-    }
-  }, [isFocus])
 
   const handleInsertVariable = () => {
     setFocus()
@@ -116,6 +120,29 @@ const Editor: FC<Props> = ({
               <div className='w-px h-3 ml-2 mr-2 bg-gray-200'></div>
               {/* Operations */}
               <div className='flex items-center space-x-2'>
+                {isSupportJinja && (
+                  <TooltipPlus
+                    popupContent={
+                      <div>
+                        <div>{t('workflow.common.enableJinja')}</div>
+                        <a className='text-[#155EEF]' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a>
+                      </div>
+                    }
+                    hideArrow
+                  >
+                    <div className={cn(editionType === EditionType.jinja2 && 'border-black/5 bg-white', 'mb-1 flex h-[22px] items-center px-1.5 rounded-[5px] border border-transparent hover:border-black/5 space-x-0.5')}>
+                      <Jinja className='w-6 h-3 text-gray-300' />
+                      <Switch
+                        size='sm'
+                        defaultValue={editionType === EditionType.jinja2}
+                        onChange={(checked) => {
+                          onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
+                        }}
+                      />
+                    </div>
+                  </TooltipPlus>
+
+                )}
                 {!readOnly && (
                   <TooltipPlus
                     popupContent={`${t('workflow.common.insertVarTip')}`}
@@ -142,57 +169,75 @@ const Editor: FC<Props> = ({
 
           {/* Min: 80 Max: 560. Header: 24 */}
           <div className={cn('pb-2', isExpand && 'flex flex-col grow')}>
-            <div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px]  overflow-y-auto')}>
-              <PromptEditor
-                instanceId={instanceId}
-                compact
-                className='min-h-[56px]'
-                style={isExpand ? { height: editorExpandHeight - 5 } : {}}
-                value={value}
-                contextBlock={{
-                  show: justVar ? false : isShowContext,
-                  selectable: !hasSetBlockStatus?.context,
-                  canNotAddContext: true,
-                }}
-                historyBlock={{
-                  show: justVar ? false : isShowHistory,
-                  selectable: !hasSetBlockStatus?.history,
-                  history: {
-                    user: 'Human',
-                    assistant: 'Assistant',
-                  },
-                }}
-                queryBlock={{
-                  show: false, // use [sys.query] instead of query block
-                  selectable: false,
-                }}
-                workflowVariableBlock={{
-                  show: true,
-                  variables: nodesOutputVars || [],
-                  workflowNodesMap: availableNodes.reduce((acc, node) => {
-                    acc[node.id] = {
-                      title: node.data.title,
-                      type: node.data.type,
-                    }
-                    if (node.data.type === BlockEnum.Start) {
-                      acc.sys = {
-                        title: t('workflow.blocks.start'),
-                        type: BlockEnum.Start,
-                      }
-                    }
-                    return acc
-                  }, {} as any),
-                }}
-                onChange={onChange}
-                onBlur={setBlur}
-                onFocus={setFocus}
-                editable={!readOnly}
-              />
-              {/* to patch Editor not support dynamic change editable status */}
-              {readOnly && <div className='absolute inset-0 z-10'></div>}
-            </div>
+            {!(isSupportJinja && editionType === EditionType.jinja2)
+              ? (
+                <div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px]  overflow-y-auto')}>
+                  <PromptEditor
+                    instanceId={instanceId}
+                    compact
+                    className='min-h-[56px]'
+                    style={isExpand ? { height: editorExpandHeight - 5 } : {}}
+                    value={value}
+                    contextBlock={{
+                      show: justVar ? false : isShowContext,
+                      selectable: !hasSetBlockStatus?.context,
+                      canNotAddContext: true,
+                    }}
+                    historyBlock={{
+                      show: justVar ? false : isShowHistory,
+                      selectable: !hasSetBlockStatus?.history,
+                      history: {
+                        user: 'Human',
+                        assistant: 'Assistant',
+                      },
+                    }}
+                    queryBlock={{
+                      show: false, // use [sys.query] instead of query block
+                      selectable: false,
+                    }}
+                    workflowVariableBlock={{
+                      show: true,
+                      variables: nodesOutputVars || [],
+                      workflowNodesMap: availableNodes.reduce((acc, node) => {
+                        acc[node.id] = {
+                          title: node.data.title,
+                          type: node.data.type,
+                        }
+                        if (node.data.type === BlockEnum.Start) {
+                          acc.sys = {
+                            title: t('workflow.blocks.start'),
+                            type: BlockEnum.Start,
+                          }
+                        }
+                        return acc
+                      }, {} as any),
+                    }}
+                    onChange={onChange}
+                    onBlur={setBlur}
+                    onFocus={setFocus}
+                    editable={!readOnly}
+                  />
+                  {/* to patch Editor not support dynamic change editable status */}
+                  {readOnly && <div className='absolute inset-0 z-10'></div>}
+                </div>
+              )
+              : (
+                <div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px]  overflow-y-auto')}>
+                  <CodeEditor
+                    availableVars={nodesOutputVars || []}
+                    varList={varList}
+                    onAddVar={handleAddVariable}
+                    isInNode
+                    readOnly={readOnly}
+                    language={CodeLanguage.python3}
+                    value={value}
+                    onChange={onChange}
+                    noWrapper
+                    isExpand={isExpand}
+                  />
+                </div>
+              )}
           </div>
-
         </div>
       </div>
     </Wrap>

+ 15 - 2
web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx

@@ -3,7 +3,8 @@ import type { FC } from 'react'
 import React, { useEffect, useState } from 'react'
 import { uniqueId } from 'lodash-es'
 import { useTranslation } from 'react-i18next'
-import type { PromptItem } from '../../../types'
+import type { PromptItem, Variable } from '../../../types'
+import { EditionType } from '../../../types'
 import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
 import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
 import TooltipPlus from '@/app/components/base/tooltip-plus'
@@ -24,6 +25,7 @@ type Props = {
   payload: PromptItem
   handleChatModeMessageRoleChange: (role: PromptRole) => void
   onPromptChange: (p: string) => void
+  onEditionTypeChange: (editionType: EditionType) => void
   onRemove: () => void
   isShowContext: boolean
   hasSetBlockStatus: {
@@ -33,6 +35,8 @@ type Props = {
   }
   availableVars: any
   availableNodes: any
+  varList: Variable[]
+  handleAddVariable: (payload: any) => void
 }
 
 const roleOptions = [
@@ -64,17 +68,21 @@ const ConfigPromptItem: FC<Props> = ({
   isChatApp,
   payload,
   onPromptChange,
+  onEditionTypeChange,
   onRemove,
   isShowContext,
   hasSetBlockStatus,
   availableVars,
   availableNodes,
+  varList,
+  handleAddVariable,
 }) => {
   const { t } = useTranslation()
   const [instanceId, setInstanceId] = useState(uniqueId())
   useEffect(() => {
     setInstanceId(`${id}-${uniqueId()}`)
   }, [id])
+
   return (
     <Editor
       className={className}
@@ -107,7 +115,7 @@ const ConfigPromptItem: FC<Props> = ({
           </TooltipPlus>
         </div>
       }
-      value={payload.text}
+      value={payload.edition_type === EditionType.jinja2 ? (payload.jinja2_text || '') : payload.text}
       onChange={onPromptChange}
       readOnly={readOnly}
       showRemove={canRemove}
@@ -118,6 +126,11 @@ const ConfigPromptItem: FC<Props> = ({
       hasSetBlockStatus={hasSetBlockStatus}
       nodesOutputVars={availableVars}
       availableNodes={availableNodes}
+      isSupportJinja
+      editionType={payload.edition_type}
+      onEditionTypeChange={onEditionTypeChange}
+      varList={varList}
+      handleAddVariable={handleAddVariable}
     />
   )
 }

+ 34 - 6
web/app/components/workflow/nodes/llm/components/config-prompt.tsx

@@ -6,8 +6,8 @@ import produce from 'immer'
 import { ReactSortable } from 'react-sortablejs'
 import { v4 as uuid4 } from 'uuid'
 import cn from 'classnames'
-import type { PromptItem, ValueSelector, Var } from '../../../types'
-import { PromptRole } from '../../../types'
+import type { PromptItem, ValueSelector, Var, Variable } from '../../../types'
+import { EditionType, PromptRole } from '../../../types'
 import useAvailableVarList from '../../_base/hooks/use-available-var-list'
 import ConfigPromptItem from './config-prompt-item'
 import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
@@ -30,6 +30,8 @@ type Props = {
     history: boolean
     query: boolean
   }
+  varList?: Variable[]
+  handleAddVariable: (payload: any) => void
 }
 
 const ConfigPrompt: FC<Props> = ({
@@ -42,10 +44,12 @@ const ConfigPrompt: FC<Props> = ({
   onChange,
   isShowContext,
   hasSetBlockStatus,
+  varList = [],
+  handleAddVariable,
 }) => {
   const { t } = useTranslation()
   const payloadWithIds = (isChatModel && Array.isArray(payload))
-    ? payload.map((item, i) => {
+    ? payload.map((item) => {
       const id = uuid4()
       return {
         id: item.id || id,
@@ -67,7 +71,16 @@ const ConfigPrompt: FC<Props> = ({
   const handleChatModePromptChange = useCallback((index: number) => {
     return (prompt: string) => {
       const newPrompt = produce(payload as PromptItem[], (draft) => {
-        draft[index].text = prompt
+        draft[index][draft[index].edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt
+      })
+      onChange(newPrompt)
+    }
+  }, [onChange, payload])
+
+  const handleChatModeEditionTypeChange = useCallback((index: number) => {
+    return (editionType: EditionType) => {
+      const newPrompt = produce(payload as PromptItem[], (draft) => {
+        draft[index].edition_type = editionType
       })
       onChange(newPrompt)
     }
@@ -106,7 +119,14 @@ const ConfigPrompt: FC<Props> = ({
 
   const handleCompletionPromptChange = useCallback((prompt: string) => {
     const newPrompt = produce(payload as PromptItem, (draft) => {
-      draft.text = prompt
+      draft[draft.edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt
+    })
+    onChange(newPrompt)
+  }, [onChange, payload])
+
+  const handleCompletionEditionTypeChange = useCallback((editionType: EditionType) => {
+    const newPrompt = produce(payload as PromptItem, (draft) => {
+      draft.edition_type = editionType
     })
     onChange(newPrompt)
   }, [onChange, payload])
@@ -161,11 +181,14 @@ const ConfigPrompt: FC<Props> = ({
                           isChatApp={isChatApp}
                           payload={item}
                           onPromptChange={handleChatModePromptChange(index)}
+                          onEditionTypeChange={handleChatModeEditionTypeChange(index)}
                           onRemove={handleRemove(index)}
                           isShowContext={isShowContext}
                           hasSetBlockStatus={hasSetBlockStatus}
                           availableVars={availableVars}
                           availableNodes={availableNodes}
+                          varList={varList}
+                          handleAddVariable={handleAddVariable}
                         />
                       </div>
 
@@ -187,7 +210,7 @@ const ConfigPrompt: FC<Props> = ({
             <Editor
               instanceId={`${nodeId}-chat-workflow-llm-prompt-editor`}
               title={<span className='capitalize'>{t(`${i18nPrefix}.prompt`)}</span>}
-              value={(payload as PromptItem).text}
+              value={(payload as PromptItem).edition_type === EditionType.basic ? (payload as PromptItem).text : ((payload as PromptItem).jinja2_text || '')}
               onChange={handleCompletionPromptChange}
               readOnly={readOnly}
               isChatModel={isChatModel}
@@ -196,6 +219,11 @@ const ConfigPrompt: FC<Props> = ({
               hasSetBlockStatus={hasSetBlockStatus}
               nodesOutputVars={availableVars}
               availableNodes={availableNodes}
+              isSupportJinja
+              editionType={(payload as PromptItem).edition_type}
+              varList={varList}
+              onEditionTypeChange={handleCompletionEditionTypeChange}
+              handleAddVariable={handleAddVariable}
             />
           </div>
         )}

+ 19 - 4
web/app/components/workflow/nodes/llm/default.ts

@@ -1,7 +1,6 @@
-import { BlockEnum } from '../../types'
-import { type NodeDefault, PromptRole } from '../../types'
+import { BlockEnum, EditionType } from '../../types'
+import { type NodeDefault, type PromptItem, PromptRole } from '../../types'
 import type { LLMNodeType } from './types'
-import type { PromptItem } from '@/models/debug'
 import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
 
 const i18nPrefix = 'workflow.errorMsg'
@@ -16,7 +15,6 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
         temperature: 0.7,
       },
     },
-    variables: [],
     prompt_template: [{
       role: PromptRole.system,
       text: '',
@@ -57,6 +55,23 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
       if (isChatModel && !!payload.memory.query_prompt_template && !payload.memory.query_prompt_template.includes('{{#sys.query#}}'))
         errorMessages = t('workflow.nodes.llm.sysQueryInUser')
     }
+
+    if (!errorMessages) {
+      const isChatModel = payload.model.mode === 'chat'
+      const isShowVars = (() => {
+        if (isChatModel)
+          return (payload.prompt_template as PromptItem[]).some(item => item.edition_type === EditionType.jinja2)
+        return (payload.prompt_template as PromptItem).edition_type === EditionType.jinja2
+      })()
+      if (isShowVars && payload.prompt_config?.jinja2_variables) {
+        payload.prompt_config?.jinja2_variables.forEach((i) => {
+          if (!errorMessages && !i.variable)
+            errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
+          if (!errorMessages && !i.value_selector.length)
+            errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
+        })
+      }
+    }
     return {
       isValid: !errorMessages,
       errorMessage: errorMessages,

+ 27 - 0
web/app/components/workflow/nodes/llm/panel.tsx

@@ -7,6 +7,8 @@ import useConfig from './use-config'
 import ResolutionPicker from './components/resolution-picker'
 import type { LLMNodeType } from './types'
 import ConfigPrompt from './components/config-prompt'
+import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
+import AddButton2 from '@/app/components/base/button/add-button'
 import Field from '@/app/components/workflow/nodes/_base/components/field'
 import Split from '@/app/components/workflow/nodes/_base/components/split'
 import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@@ -44,7 +46,12 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
     filterVar,
     availableVars,
     availableNodes,
+    isShowVars,
     handlePromptChange,
+    handleAddEmptyVariable,
+    handleAddVariable,
+    handleVarListChange,
+    handleVarNameChange,
     handleSyeQueryChange,
     handleMemoryChange,
     handleVisionResolutionEnabledChange,
@@ -169,9 +176,29 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
             payload={inputs.prompt_template}
             onChange={handlePromptChange}
             hasSetBlockStatus={hasSetBlockStatus}
+            varList={inputs.prompt_config?.jinja2_variables || []}
+            handleAddVariable={handleAddVariable}
           />
         )}
 
+        {isShowVars && (
+          <Field
+            title={t('workflow.nodes.templateTransform.inputVars')}
+            operations={
+              !readOnly ? <AddButton2 onClick={handleAddEmptyVariable} /> : undefined
+            }
+          >
+            <VarList
+              nodeId={id}
+              readonly={readOnly}
+              list={inputs.prompt_config?.jinja2_variables || []}
+              onChange={handleVarListChange}
+              onVarNameChange={handleVarNameChange}
+              filterVar={filterVar}
+            />
+          </Field>
+        )}
+
         {/* Memory put place examples. */}
         {isChatMode && isChatModel && !!inputs.memory && (
           <div className='mt-4'>

+ 3 - 1
web/app/components/workflow/nodes/llm/types.ts

@@ -3,8 +3,10 @@ import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Va
 
 export type LLMNodeType = CommonNodeType & {
   model: ModelConfig
-  variables: Variable[]
   prompt_template: PromptItem[] | PromptItem
+  prompt_config?: {
+    jinja2_variables?: Variable[]
+  }
   memory?: Memory
   context: {
     enabled: boolean

+ 94 - 33
web/app/components/workflow/nodes/llm/use-config.ts

@@ -1,8 +1,7 @@
 import { useCallback, useEffect, useRef, useState } from 'react'
 import produce from 'immer'
-import useVarList from '../_base/hooks/use-var-list'
-import { VarType } from '../../types'
-import type { Memory, ValueSelector, Var } from '../../types'
+import { EditionType, VarType } from '../../types'
+import type { Memory, PromptItem, ValueSelector, Var, Variable } from '../../types'
 import { useStore } from '../../store'
 import {
   useIsChatMode,
@@ -18,7 +17,6 @@ import {
 } from '@/app/components/header/account-setting/model-provider-page/declarations'
 import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
 import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
-import type { PromptItem } from '@/models/debug'
 import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
 import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
 
@@ -29,20 +27,21 @@ const useConfig = (id: string, payload: LLMNodeType) => {
   const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
   const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' })
   const { inputs, setInputs: doSetInputs } = useNodeCrud<LLMNodeType>(id, payload)
+  const inputRef = useRef(inputs)
+
   const setInputs = useCallback((newInputs: LLMNodeType) => {
     if (newInputs.memory && !newInputs.memory.role_prefix) {
       const newPayload = produce(newInputs, (draft) => {
         draft.memory!.role_prefix = defaultRolePrefix
       })
       doSetInputs(newPayload)
+      inputRef.current = newPayload
       return
     }
     doSetInputs(newInputs)
+    inputRef.current = newInputs
   }, [doSetInputs, defaultRolePrefix])
-  const inputRef = useRef(inputs)
-  useEffect(() => {
-    inputRef.current = inputs
-  }, [inputs])
+
   // model
   const model = inputs.model
   const modelMode = inputs.model?.mode
@@ -178,11 +177,80 @@ const useConfig = (id: string, payload: LLMNodeType) => {
     }
   // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [isShowVisionConfig, modelChanged])
+
   // variables
-  const { handleVarListChange, handleAddVariable } = useVarList<LLMNodeType>({
-    inputs,
-    setInputs,
-  })
+  const isShowVars = (() => {
+    if (isChatModel)
+      return (inputs.prompt_template as PromptItem[]).some(item => item.edition_type === EditionType.jinja2)
+
+    return (inputs.prompt_template as PromptItem).edition_type === EditionType.jinja2
+  })()
+  const handleAddEmptyVariable = useCallback(() => {
+    const newInputs = produce(inputRef.current, (draft) => {
+      if (!draft.prompt_config) {
+        draft.prompt_config = {
+          jinja2_variables: [],
+        }
+      }
+      if (!draft.prompt_config.jinja2_variables)
+        draft.prompt_config.jinja2_variables = []
+
+      draft.prompt_config.jinja2_variables.push({
+        variable: '',
+        value_selector: [],
+      })
+    })
+    setInputs(newInputs)
+  }, [setInputs])
+
+  const handleAddVariable = useCallback((payload: Variable) => {
+    const newInputs = produce(inputRef.current, (draft) => {
+      if (!draft.prompt_config) {
+        draft.prompt_config = {
+          jinja2_variables: [],
+        }
+      }
+      if (!draft.prompt_config.jinja2_variables)
+        draft.prompt_config.jinja2_variables = []
+
+      draft.prompt_config.jinja2_variables.push(payload)
+    })
+    setInputs(newInputs)
+  }, [setInputs])
+
+  const handleVarListChange = useCallback((newList: Variable[]) => {
+    const newInputs = produce(inputRef.current, (draft) => {
+      if (!draft.prompt_config) {
+        draft.prompt_config = {
+          jinja2_variables: [],
+        }
+      }
+      if (!draft.prompt_config.jinja2_variables)
+        draft.prompt_config.jinja2_variables = []
+
+      draft.prompt_config.jinja2_variables = newList
+    })
+    setInputs(newInputs)
+  }, [setInputs])
+
+  const handleVarNameChange = useCallback((oldName: string, newName: string) => {
+    const newInputs = produce(inputRef.current, (draft) => {
+      if (isChatModel) {
+        const promptTemplate = draft.prompt_template as PromptItem[]
+        promptTemplate.filter(item => item.edition_type === EditionType.jinja2).forEach((item) => {
+          item.jinja2_text = (item.jinja2_text || '').replaceAll(`{{ ${oldName} }}`, `{{ ${newName} }}`)
+        })
+      }
+      else {
+        if ((draft.prompt_template as PromptItem).edition_type !== EditionType.jinja2)
+          return
+
+        const promptTemplate = draft.prompt_template as PromptItem
+        promptTemplate.jinja2_text = (promptTemplate.jinja2_text || '').replaceAll(`{{ ${oldName} }}`, `{{ ${newName} }}`)
+      }
+    })
+    setInputs(newInputs)
+  }, [isChatModel, setInputs])
 
   // context
   const handleContextVarChange = useCallback((newVar: ValueSelector | string) => {
@@ -194,11 +262,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
   }, [inputs, setInputs])
 
   const handlePromptChange = useCallback((newPrompt: PromptItem[] | PromptItem) => {
-    const newInputs = produce(inputs, (draft) => {
+    const newInputs = produce(inputRef.current, (draft) => {
       draft.prompt_template = newPrompt
     })
     setInputs(newInputs)
-  }, [inputs, setInputs])
+  }, [setInputs])
 
   const handleMemoryChange = useCallback((newMemory?: Memory) => {
     const newInputs = produce(inputs, (draft) => {
@@ -286,6 +354,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
     runInputData,
     setRunInputData,
     runResult,
+    toVarInputs,
   } = useOneStepRun<LLMNodeType>({
     id,
     data: inputs,
@@ -295,23 +364,6 @@ const useConfig = (id: string, payload: LLMNodeType) => {
     },
   })
 
-  // const handleRun = (submitData: Record<string, any>) => {
-  //   console.log(submitData)
-  //   const res = produce(submitData, (draft) => {
-  //     debugger
-  //     if (draft.contexts) {
-  //       draft['#context#'] = draft.contexts
-  //       delete draft.contexts
-  //     }
-  //     if (draft.visionFiles) {
-  //       draft['#files#'] = draft.visionFiles
-  //       delete draft.visionFiles
-  //     }
-  //   })
-
-  //   doHandleRun(res)
-  // }
-
   const inputVarValues = (() => {
     const vars: Record<string, any> = {}
     Object.keys(runInputData)
@@ -348,7 +400,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
   }, [runInputData, setRunInputData])
 
   const allVarStrArr = (() => {
-    const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).map(item => item.text) : [(inputs.prompt_template as PromptItem).text]
+    const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).filter(item => item.edition_type !== EditionType.jinja2).map(item => item.text) : [(inputs.prompt_template as PromptItem).text]
     if (isChatMode && isChatModel && !!inputs.memory) {
       arr.push('{{#sys.query#}}')
       arr.push(inputs.memory.query_prompt_template)
@@ -357,7 +409,13 @@ const useConfig = (id: string, payload: LLMNodeType) => {
     return arr
   })()
 
-  const varInputs = getInputVars(allVarStrArr)
+  const varInputs = (() => {
+    const vars = getInputVars(allVarStrArr)
+    if (isShowVars)
+      return [...vars, ...toVarInputs(inputs.prompt_config?.jinja2_variables || [])]
+
+    return vars
+  })()
 
   return {
     readOnly,
@@ -370,8 +428,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
     isShowVisionConfig,
     handleModelChanged,
     handleCompletionParamsChange,
+    isShowVars,
     handleVarListChange,
+    handleVarNameChange,
     handleAddVariable,
+    handleAddEmptyVariable,
     handleContextVarChange,
     filterInputVar,
     filterVar,

+ 2 - 1
web/app/components/workflow/nodes/template-transform/panel.tsx

@@ -26,6 +26,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
   const {
     readOnly,
     inputs,
+    availableVars,
     handleVarListChange,
     handleVarNameChange,
     handleAddVariable,
@@ -65,7 +66,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
         </Field>
         <Split />
         <CodeEditor
-          nodeId={id}
+          availableVars={availableVars}
           varList={inputs.variables}
           onAddVar={handleAddVariable}
           isInNode

+ 7 - 0
web/app/components/workflow/nodes/template-transform/use-config.ts

@@ -10,6 +10,7 @@ import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-s
 import {
   useNodesReadOnly,
 } from '@/app/components/workflow/hooks'
+import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
 
 const useConfig = (id: string, payload: TemplateTransformNodeType) => {
   const { nodesReadOnly: readOnly } = useNodesReadOnly()
@@ -22,6 +23,11 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
     inputsRef.current = newPayload
   }, [doSetInputs])
 
+  const { availableVars } = useAvailableVarList(id, {
+    onlyLeafNodeVar: false,
+    filterVar: () => true,
+  })
+
   const { handleAddVariable: handleAddEmptyVariable } = useVarList<TemplateTransformNodeType>({
     inputs,
     setInputs,
@@ -108,6 +114,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
   return {
     readOnly,
     inputs,
+    availableVars,
     handleVarListChange,
     handleVarNameChange,
     handleAddVariable,

+ 7 - 0
web/app/components/workflow/types.ts

@@ -131,10 +131,17 @@ export enum PromptRole {
   assistant = 'assistant',
 }
 
+export enum EditionType {
+  basic = 'basic',
+  jinja2 = 'jinja2',
+}
+
 export type PromptItem = {
   id?: string
   role?: PromptRole
   text: string
+  edition_type?: EditionType
+  jinja2_text?: string
 }
 
 export enum MemoryRole {

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

@@ -52,6 +52,8 @@ const translation = {
     jinjaEditorPlaceholder: 'Type \'/\' or \'{\' to insert variable',
     viewOnly: 'View Only',
     showRunHistory: 'Show Run History',
+    enableJinja: 'Enable Jinja template support',
+    learnMore: 'Learn More',
     copy: 'Copy',
     duplicate: 'Duplicate',
     addBlock: 'Add Block',

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

@@ -52,6 +52,8 @@ const translation = {
     jinjaEditorPlaceholder: '输入 “/” 或 “{” 插入变量',
     viewOnly: '只读',
     showRunHistory: '显示运行历史',
+    enableJinja: '开启支持 Jinja 模板',
+    learnMore: '了解更多',
     copy: '拷贝',
     duplicate: '复制',
     addBlock: '添加节点',