feat: llm support jinja fe (#4260)

This commit is contained in:
Joel 2024-05-10 18:14:05 +08:00 committed by GitHub
parent 6b99075dc8
commit 01555463d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 621 additions and 177 deletions

View File

@ -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>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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"
}

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,23 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import Editor, { loader } from '@monaco-editor/react' import Editor, { loader } from '@monaco-editor/react'
import React, { useEffect, useRef, useState } from 'react'
import React, { useRef } from 'react' import cn from 'classnames'
import Base from '../base' import Base from '../base'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import './style.css' import './style.css'
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482 // load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
loader.config({ paths: { vs: '/vs' } }) loader.config({ paths: { vs: '/vs' } })
const CODE_EDITOR_LINE_HEIGHT = 18
export type Props = { export type Props = {
value?: string | object value?: string | object
placeholder?: string placeholder?: string
onChange?: (value: string) => void onChange?: (value: string) => void
title: JSX.Element title?: JSX.Element
language: CodeLanguage language: CodeLanguage
headerRight?: JSX.Element headerRight?: JSX.Element
readOnly?: boolean readOnly?: boolean
@ -22,6 +25,8 @@ export type Props = {
height?: number height?: number
isInNode?: boolean isInNode?: boolean
onMount?: (editor: any, monaco: any) => void onMount?: (editor: any, monaco: any) => void
noWrapper?: boolean
isExpand?: boolean
} }
const languageMap = { const languageMap = {
@ -30,11 +35,20 @@ const languageMap = {
[CodeLanguage.json]: 'json', [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> = ({ const CodeEditor: FC<Props> = ({
value = '', value = '',
placeholder = '', placeholder = '',
onChange = () => { }, onChange = () => { },
title, title = '',
headerRight, headerRight,
language, language,
readOnly, readOnly,
@ -42,16 +56,37 @@ const CodeEditor: FC<Props> = ({
height, height,
isInNode, isInNode,
onMount, onMount,
noWrapper,
isExpand,
}) => { }) => {
const [isFocus, setIsFocus] = React.useState(false) 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) => { const handleEditorChange = (value: string | undefined) => {
onChange(value || '') onChange(value || '')
setTimeout(() => {
resizeEditorToContent()
}, 10)
} }
const editorRef = useRef(null)
const handleEditorDidMount = (editor: any, monaco: any) => { const handleEditorDidMount = (editor: any, monaco: any) => {
editorRef.current = editor editorRef.current = editor
resizeEditorToContent()
editor.onDidFocusEditorText(() => { editor.onDidFocusEditorText(() => {
setIsFocus(true) setIsFocus(true)
@ -60,6 +95,8 @@ const CodeEditor: FC<Props> = ({
setIsFocus(false) setIsFocus(false)
}) })
monaco.editor.defineTheme('default-theme', DEFAULT_THEME)
monaco.editor.defineTheme('blur-theme', { monaco.editor.defineTheme('blur-theme', {
base: 'vs', base: 'vs',
inherit: true, 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) onMount?.(editor, monaco)
setIsMounted(true)
} }
const outPutValue = (() => { const outPutValue = (() => {
@ -92,43 +132,63 @@ const CodeEditor: FC<Props> = ({
} }
})() })()
return ( const theme = (() => {
<div> if (noWrapper)
<Base return 'default-theme'
className='relative'
title={title} 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} value={outPutValue}
headerRight={headerRight} onChange={handleEditorChange}
isFocus={isFocus && !readOnly} // https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
minHeight={height || 200} options={{
isInNode={isInNode} readOnly,
> domReadOnly: true,
<> quickSuggestions: false,
{/* https://www.npmjs.com/package/@monaco-editor/react */} minimap: { enabled: false },
<Editor lineNumbersMinChars: 1, // would change line num width
className='h-full' wordWrap: 'on', // auto line wrap
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'} // lineNumbers: (num) => {
language={languageMap[language] || 'javascript'} // return <div>{num}</div>
theme={isFocus ? 'focus-theme' : 'blur-theme'} // }
}}
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} value={outPutValue}
onChange={handleEditorChange} headerRight={headerRight}
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html isFocus={isFocus && !readOnly}
options={{ minHeight={minHeight}
readOnly, isInNode={isInNode}
domReadOnly: true, >
quickSuggestions: false, {main}
minimap: { enabled: false }, </Base>
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>
</div> </div>
) )
} }

View File

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

View File

@ -1,16 +1,19 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useRef } from 'react'
import cn from 'classnames' import cn from 'classnames'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { import { BlockEnum, EditionType } from '../../../../types'
BlockEnum, import type {
type Node, Node,
type NodeOutPutVar, NodeOutPutVar,
Variable,
} from '../../../../types' } from '../../../../types'
import Wrap from '../editor/wrap' import Wrap from '../editor/wrap'
import { CodeLanguage } from '../../../code/types'
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn' 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 useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
import PromptEditor from '@/app/components/base/prompt-editor' 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 { 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 { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import TooltipPlus from '@/app/components/base/tooltip-plus' 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 = { type Props = {
className?: string className?: string
headerClassName?: string headerClassName?: string
@ -42,6 +49,12 @@ type Props = {
} }
nodesOutputVars?: NodeOutPutVar[] nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[] availableNodes?: Node[]
// for jinja
isSupportJinja?: boolean
editionType?: EditionType
onEditionTypeChange?: (editionType: EditionType) => void
varList?: Variable[]
handleAddVariable?: (payload: any) => void
} }
const Editor: FC<Props> = ({ const Editor: FC<Props> = ({
@ -61,6 +74,11 @@ const Editor: FC<Props> = ({
hasSetBlockStatus, hasSetBlockStatus,
nodesOutputVars, nodesOutputVars,
availableNodes = [], availableNodes = [],
isSupportJinja,
editionType,
onEditionTypeChange,
varList = [],
handleAddVariable,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
@ -85,20 +103,6 @@ const Editor: FC<Props> = ({
setTrue: setFocus, setTrue: setFocus,
setFalse: setBlur, setFalse: setBlur,
}] = useBoolean(false) }] = 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 = () => { const handleInsertVariable = () => {
setFocus() setFocus()
@ -116,6 +120,29 @@ const Editor: FC<Props> = ({
<div className='w-px h-3 ml-2 mr-2 bg-gray-200'></div> <div className='w-px h-3 ml-2 mr-2 bg-gray-200'></div>
{/* Operations */} {/* Operations */}
<div className='flex items-center space-x-2'> <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 && ( {!readOnly && (
<TooltipPlus <TooltipPlus
popupContent={`${t('workflow.common.insertVarTip')}`} popupContent={`${t('workflow.common.insertVarTip')}`}
@ -142,57 +169,75 @@ const Editor: FC<Props> = ({
{/* Min: 80 Max: 560. Header: 24 */} {/* Min: 80 Max: 560. Header: 24 */}
<div className={cn('pb-2', isExpand && 'flex flex-col grow')}> <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')}> {!(isSupportJinja && editionType === EditionType.jinja2)
<PromptEditor ? (
instanceId={instanceId} <div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
compact <PromptEditor
className='min-h-[56px]' instanceId={instanceId}
style={isExpand ? { height: editorExpandHeight - 5 } : {}} compact
value={value} className='min-h-[56px]'
contextBlock={{ style={isExpand ? { height: editorExpandHeight - 5 } : {}}
show: justVar ? false : isShowContext, value={value}
selectable: !hasSetBlockStatus?.context, contextBlock={{
canNotAddContext: true, show: justVar ? false : isShowContext,
}} selectable: !hasSetBlockStatus?.context,
historyBlock={{ canNotAddContext: true,
show: justVar ? false : isShowHistory, }}
selectable: !hasSetBlockStatus?.history, historyBlock={{
history: { show: justVar ? false : isShowHistory,
user: 'Human', selectable: !hasSetBlockStatus?.history,
assistant: 'Assistant', history: {
}, user: 'Human',
}} assistant: 'Assistant',
queryBlock={{ },
show: false, // use [sys.query] instead of query block }}
selectable: false, queryBlock={{
}} show: false, // use [sys.query] instead of query block
workflowVariableBlock={{ selectable: false,
show: true, }}
variables: nodesOutputVars || [], workflowVariableBlock={{
workflowNodesMap: availableNodes.reduce((acc, node) => { show: true,
acc[node.id] = { variables: nodesOutputVars || [],
title: node.data.title, workflowNodesMap: availableNodes.reduce((acc, node) => {
type: node.data.type, acc[node.id] = {
} title: node.data.title,
if (node.data.type === BlockEnum.Start) { type: node.data.type,
acc.sys = { }
title: t('workflow.blocks.start'), if (node.data.type === BlockEnum.Start) {
type: BlockEnum.Start, acc.sys = {
} title: t('workflow.blocks.start'),
} type: BlockEnum.Start,
return acc }
}, {} as any), }
}} return acc
onChange={onChange} }, {} as any),
onBlur={setBlur} }}
onFocus={setFocus} onChange={onChange}
editable={!readOnly} onBlur={setBlur}
/> onFocus={setFocus}
{/* to patch Editor not support dynamic change editable status */} editable={!readOnly}
{readOnly && <div className='absolute inset-0 z-10'></div>} />
</div> {/* 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> </div>
</div> </div>
</Wrap> </Wrap>

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import { BlockEnum } from '../../types' import { BlockEnum, EditionType } from '../../types'
import { type NodeDefault, PromptRole } from '../../types' import { type NodeDefault, type PromptItem, PromptRole } from '../../types'
import type { LLMNodeType } 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' import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg' const i18nPrefix = 'workflow.errorMsg'
@ -16,7 +15,6 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
temperature: 0.7, temperature: 0.7,
}, },
}, },
variables: [],
prompt_template: [{ prompt_template: [{
role: PromptRole.system, role: PromptRole.system,
text: '', text: '',
@ -57,6 +55,23 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
if (isChatModel && !!payload.memory.query_prompt_template && !payload.memory.query_prompt_template.includes('{{#sys.query#}}')) if (isChatModel && !!payload.memory.query_prompt_template && !payload.memory.query_prompt_template.includes('{{#sys.query#}}'))
errorMessages = t('workflow.nodes.llm.sysQueryInUser') 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 { return {
isValid: !errorMessages, isValid: !errorMessages,
errorMessage: errorMessages, errorMessage: errorMessages,

View File

@ -7,6 +7,8 @@ import useConfig from './use-config'
import ResolutionPicker from './components/resolution-picker' import ResolutionPicker from './components/resolution-picker'
import type { LLMNodeType } from './types' import type { LLMNodeType } from './types'
import ConfigPrompt from './components/config-prompt' 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 Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split' import Split from '@/app/components/workflow/nodes/_base/components/split'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@ -44,7 +46,12 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
filterVar, filterVar,
availableVars, availableVars,
availableNodes, availableNodes,
isShowVars,
handlePromptChange, handlePromptChange,
handleAddEmptyVariable,
handleAddVariable,
handleVarListChange,
handleVarNameChange,
handleSyeQueryChange, handleSyeQueryChange,
handleMemoryChange, handleMemoryChange,
handleVisionResolutionEnabledChange, handleVisionResolutionEnabledChange,
@ -169,9 +176,29 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
payload={inputs.prompt_template} payload={inputs.prompt_template}
onChange={handlePromptChange} onChange={handlePromptChange}
hasSetBlockStatus={hasSetBlockStatus} 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. */} {/* Memory put place examples. */}
{isChatMode && isChatModel && !!inputs.memory && ( {isChatMode && isChatModel && !!inputs.memory && (
<div className='mt-4'> <div className='mt-4'>

View File

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

View File

@ -1,8 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import produce from 'immer' import produce from 'immer'
import useVarList from '../_base/hooks/use-var-list' import { EditionType, VarType } from '../../types'
import { VarType } from '../../types' import type { Memory, PromptItem, ValueSelector, Var, Variable } from '../../types'
import type { Memory, ValueSelector, Var } from '../../types'
import { useStore } from '../../store' import { useStore } from '../../store'
import { import {
useIsChatMode, useIsChatMode,
@ -18,7 +17,6 @@ import {
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' 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 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 { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/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 defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' }) const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' })
const { inputs, setInputs: doSetInputs } = useNodeCrud<LLMNodeType>(id, payload) const { inputs, setInputs: doSetInputs } = useNodeCrud<LLMNodeType>(id, payload)
const inputRef = useRef(inputs)
const setInputs = useCallback((newInputs: LLMNodeType) => { const setInputs = useCallback((newInputs: LLMNodeType) => {
if (newInputs.memory && !newInputs.memory.role_prefix) { if (newInputs.memory && !newInputs.memory.role_prefix) {
const newPayload = produce(newInputs, (draft) => { const newPayload = produce(newInputs, (draft) => {
draft.memory!.role_prefix = defaultRolePrefix draft.memory!.role_prefix = defaultRolePrefix
}) })
doSetInputs(newPayload) doSetInputs(newPayload)
inputRef.current = newPayload
return return
} }
doSetInputs(newInputs) doSetInputs(newInputs)
inputRef.current = newInputs
}, [doSetInputs, defaultRolePrefix]) }, [doSetInputs, defaultRolePrefix])
const inputRef = useRef(inputs)
useEffect(() => {
inputRef.current = inputs
}, [inputs])
// model // model
const model = inputs.model const model = inputs.model
const modelMode = inputs.model?.mode const modelMode = inputs.model?.mode
@ -178,11 +177,80 @@ const useConfig = (id: string, payload: LLMNodeType) => {
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isShowVisionConfig, modelChanged]) }, [isShowVisionConfig, modelChanged])
// variables // variables
const { handleVarListChange, handleAddVariable } = useVarList<LLMNodeType>({ const isShowVars = (() => {
inputs, if (isChatModel)
setInputs, 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 // context
const handleContextVarChange = useCallback((newVar: ValueSelector | string) => { const handleContextVarChange = useCallback((newVar: ValueSelector | string) => {
@ -194,11 +262,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}, [inputs, setInputs]) }, [inputs, setInputs])
const handlePromptChange = useCallback((newPrompt: PromptItem[] | PromptItem) => { const handlePromptChange = useCallback((newPrompt: PromptItem[] | PromptItem) => {
const newInputs = produce(inputs, (draft) => { const newInputs = produce(inputRef.current, (draft) => {
draft.prompt_template = newPrompt draft.prompt_template = newPrompt
}) })
setInputs(newInputs) setInputs(newInputs)
}, [inputs, setInputs]) }, [setInputs])
const handleMemoryChange = useCallback((newMemory?: Memory) => { const handleMemoryChange = useCallback((newMemory?: Memory) => {
const newInputs = produce(inputs, (draft) => { const newInputs = produce(inputs, (draft) => {
@ -286,6 +354,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
runInputData, runInputData,
setRunInputData, setRunInputData,
runResult, runResult,
toVarInputs,
} = useOneStepRun<LLMNodeType>({ } = useOneStepRun<LLMNodeType>({
id, id,
data: inputs, 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 inputVarValues = (() => {
const vars: Record<string, any> = {} const vars: Record<string, any> = {}
Object.keys(runInputData) Object.keys(runInputData)
@ -348,7 +400,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}, [runInputData, setRunInputData]) }, [runInputData, setRunInputData])
const allVarStrArr = (() => { 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) { if (isChatMode && isChatModel && !!inputs.memory) {
arr.push('{{#sys.query#}}') arr.push('{{#sys.query#}}')
arr.push(inputs.memory.query_prompt_template) arr.push(inputs.memory.query_prompt_template)
@ -357,7 +409,13 @@ const useConfig = (id: string, payload: LLMNodeType) => {
return arr 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 { return {
readOnly, readOnly,
@ -370,8 +428,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
isShowVisionConfig, isShowVisionConfig,
handleModelChanged, handleModelChanged,
handleCompletionParamsChange, handleCompletionParamsChange,
isShowVars,
handleVarListChange, handleVarListChange,
handleVarNameChange,
handleAddVariable, handleAddVariable,
handleAddEmptyVariable,
handleContextVarChange, handleContextVarChange,
filterInputVar, filterInputVar,
filterVar, filterVar,

View File

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

View File

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

View File

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

View File

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

View File

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