From 01555463d2bf4866a2503fe9706e54706d073bc5 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 10 May 2024 18:14:05 +0800 Subject: [PATCH] feat: llm support jinja fe (#4260) --- .../icons/assets/vender/workflow/jinja.svg | 13 ++ .../base/icons/src/vender/workflow/Jinja.json | 98 ++++++++++ .../base/icons/src/vender/workflow/Jinja.tsx | 16 ++ .../base/icons/src/vender/workflow/index.ts | 1 + web/app/components/base/switch/index.tsx | 5 +- .../components/base/tooltip-plus/index.tsx | 57 +++++- .../code-editor/editor-support-vars.tsx | 22 +-- .../components/editor/code-editor/index.tsx | 140 ++++++++++---- .../components/editor/code-editor/style.css | 4 + .../nodes/_base/components/prompt/editor.tsx | 183 +++++++++++------- .../llm/components/config-prompt-item.tsx | 17 +- .../nodes/llm/components/config-prompt.tsx | 40 +++- .../components/workflow/nodes/llm/default.ts | 23 ++- .../components/workflow/nodes/llm/panel.tsx | 27 +++ .../components/workflow/nodes/llm/types.ts | 4 +- .../workflow/nodes/llm/use-config.ts | 127 ++++++++---- .../nodes/template-transform/panel.tsx | 3 +- .../nodes/template-transform/use-config.ts | 7 + web/app/components/workflow/types.ts | 7 + web/i18n/en-US/workflow.ts | 2 + web/i18n/zh-Hans/workflow.ts | 2 + 21 files changed, 621 insertions(+), 177 deletions(-) create mode 100644 web/app/components/base/icons/assets/vender/workflow/jinja.svg create mode 100644 web/app/components/base/icons/src/vender/workflow/Jinja.json create mode 100644 web/app/components/base/icons/src/vender/workflow/Jinja.tsx diff --git a/web/app/components/base/icons/assets/vender/workflow/jinja.svg b/web/app/components/base/icons/assets/vender/workflow/jinja.svg new file mode 100644 index 0000000000..5b40f30ed5 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/workflow/jinja.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/app/components/base/icons/src/vender/workflow/Jinja.json b/web/app/components/base/icons/src/vender/workflow/Jinja.json new file mode 100644 index 0000000000..ba46cb9ca6 --- /dev/null +++ b/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" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/workflow/Jinja.tsx b/web/app/components/base/icons/src/vender/workflow/Jinja.tsx new file mode 100644 index 0000000000..ed819ea27b --- /dev/null +++ b/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, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Jinja' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/workflow/index.ts b/web/app/components/base/icons/src/vender/workflow/index.ts index a0ebc30014..bb79b2c045 100644 --- a/web/app/components/base/icons/src/vender/workflow/index.ts +++ b/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' diff --git a/web/app/components/base/switch/index.tsx b/web/app/components/base/switch/index.tsx index fec88ecdde..6794e51efd 100644 --- a/web/app/components/base/switch/index.tsx +++ b/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 ( = ({ 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 ( = ({ > 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} -
+
triggerMethod === 'hover' && setHoverPopup()} + onMouseLeave={() => triggerMethod === 'hover' && handleLeave(false)} + > {popupContent} {!hideArrow && arrow}
diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx index d3f5547781..c084a838ba 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx +++ b/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 = ({ - 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 = ({ 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 = ({ value_selector: varValue, } - onAddVar(newVar) + onAddVar?.(newVar) } const editor: any = editorRef.current const monaco: any = monacoRef.current @@ -143,7 +139,7 @@ const CodeEditor: FC = ({ } return ( -
+
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 = ({ value = '', placeholder = '', onChange = () => { }, - title, + title = '', headerRight, language, readOnly, @@ -42,16 +56,37 @@ const CodeEditor: FC = ({ 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(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 = ({ 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 = ({ }, }) + 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 = ({ } })() - return ( -
- { + if (noWrapper) + return 'default-theme' + + return isFocus ? 'focus-theme' : 'blur-theme' + })() + + const main = ( + <> + {/* https://www.npmjs.com/package/@monaco-editor/react */} + - <> - {/* https://www.npmjs.com/package/@monaco-editor/react */} - { + // return
{num}
+ // } + }} + onMount={handleEditorDidMount} + /> + {!outPutValue &&
{placeholder}
} + + ) + + return ( +
+ {noWrapper + ?
+ {main} +
+ : ( + { - // return
{num}
- // } - }} - onMount={handleEditorDidMount} - /> - {!outPutValue &&
{placeholder}
} - - + headerRight={headerRight} + isFocus={isFocus && !readOnly} + minHeight={minHeight} + isInNode={isInNode} + > + {main} + + )}
) } diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/style.css b/web/app/components/workflow/nodes/_base/components/editor/code-editor/style.css index 229146ce18..3a6624267a 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/style.css +++ b/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; diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index 779bf76197..08a714e385 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/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 = ({ @@ -61,6 +74,11 @@ const Editor: FC = ({ hasSetBlockStatus, nodesOutputVars, availableNodes = [], + isSupportJinja, + editionType, + onEditionTypeChange, + varList = [], + handleAddVariable, }) => { const { t } = useTranslation() const { eventEmitter } = useEventEmitterContextContext() @@ -85,20 +103,6 @@ const Editor: FC = ({ 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 = ({
{/* Operations */}
+ {isSupportJinja && ( + +
{t('workflow.common.enableJinja')}
+ {t('workflow.common.learnMore')} +
+ } + hideArrow + > +
+ + { + onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic) + }} + /> +
+ + + )} {!readOnly && ( = ({ {/* Min: 80 Max: 560. Header: 24 */}
-
- { - 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 &&
} -
+ {!(isSupportJinja && editionType === EditionType.jinja2) + ? ( +
+ { + 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 &&
} +
+ ) + : ( +
+ +
+ )}
-
diff --git a/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx b/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx index 58478591dd..6a6ef1e958 100644 --- a/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx +++ b/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 = ({ 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 ( = ({
} - 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 = ({ hasSetBlockStatus={hasSetBlockStatus} nodesOutputVars={availableVars} availableNodes={availableNodes} + isSupportJinja + editionType={payload.edition_type} + onEditionTypeChange={onEditionTypeChange} + varList={varList} + handleAddVariable={handleAddVariable} /> ) } diff --git a/web/app/components/workflow/nodes/llm/components/config-prompt.tsx b/web/app/components/workflow/nodes/llm/components/config-prompt.tsx index 6c56aa656c..b79ecfa62a 100644 --- a/web/app/components/workflow/nodes/llm/components/config-prompt.tsx +++ b/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 = ({ @@ -42,10 +44,12 @@ const ConfigPrompt: FC = ({ 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 = ({ 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 = ({ 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 = ({ 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} />
@@ -187,7 +210,7 @@ const ConfigPrompt: FC = ({ {t(`${i18nPrefix}.prompt`)}} - 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 = ({ hasSetBlockStatus={hasSetBlockStatus} nodesOutputVars={availableVars} availableNodes={availableNodes} + isSupportJinja + editionType={(payload as PromptItem).edition_type} + varList={varList} + onEditionTypeChange={handleCompletionEditionTypeChange} + handleAddVariable={handleAddVariable} /> )} diff --git a/web/app/components/workflow/nodes/llm/default.ts b/web/app/components/workflow/nodes/llm/default.ts index 8ad6d86260..e68b0cb318 100644 --- a/web/app/components/workflow/nodes/llm/default.ts +++ b/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 = { temperature: 0.7, }, }, - variables: [], prompt_template: [{ role: PromptRole.system, text: '', @@ -57,6 +55,23 @@ const nodeDefault: NodeDefault = { 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, diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index cb1e3b767c..89a5a0eb2b 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/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> = ({ filterVar, availableVars, availableNodes, + isShowVars, handlePromptChange, + handleAddEmptyVariable, + handleAddVariable, + handleVarListChange, + handleVarNameChange, handleSyeQueryChange, handleMemoryChange, handleVisionResolutionEnabledChange, @@ -169,9 +176,29 @@ const Panel: FC> = ({ payload={inputs.prompt_template} onChange={handlePromptChange} hasSetBlockStatus={hasSetBlockStatus} + varList={inputs.prompt_config?.jinja2_variables || []} + handleAddVariable={handleAddVariable} /> )} + {isShowVars && ( + : undefined + } + > + + + )} + {/* Memory put place examples. */} {isChatMode && isChatModel && !!inputs.memory && (
diff --git a/web/app/components/workflow/nodes/llm/types.ts b/web/app/components/workflow/nodes/llm/types.ts index 97261ca95a..0ada4d3728 100644 --- a/web/app/components/workflow/nodes/llm/types.ts +++ b/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 diff --git a/web/app/components/workflow/nodes/llm/use-config.ts b/web/app/components/workflow/nodes/llm/use-config.ts index 8ccbb50cca..a93bf63e47 100644 --- a/web/app/components/workflow/nodes/llm/use-config.ts +++ b/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(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({ - 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({ id, data: inputs, @@ -295,23 +364,6 @@ const useConfig = (id: string, payload: LLMNodeType) => { }, }) - // const handleRun = (submitData: Record) => { - // 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 = {} 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, diff --git a/web/app/components/workflow/nodes/template-transform/panel.tsx b/web/app/components/workflow/nodes/template-transform/panel.tsx index 70fd4806e4..e3e54419ec 100644 --- a/web/app/components/workflow/nodes/template-transform/panel.tsx +++ b/web/app/components/workflow/nodes/template-transform/panel.tsx @@ -26,6 +26,7 @@ const Panel: FC> = ({ const { readOnly, inputs, + availableVars, handleVarListChange, handleVarNameChange, handleAddVariable, @@ -65,7 +66,7 @@ const Panel: FC> = ({ { 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({ inputs, setInputs, @@ -108,6 +114,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { return { readOnly, inputs, + availableVars, handleVarListChange, handleVarNameChange, handleAddVariable, diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index f0b5e08c6c..c21887a5bf 100644 --- a/web/app/components/workflow/types.ts +++ b/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 { diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 5093b6631d..206bae5400 100644 --- a/web/i18n/en-US/workflow.ts +++ b/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', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index baae846376..781ff3b49d 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -52,6 +52,8 @@ const translation = { jinjaEditorPlaceholder: '输入 “/” 或 “{” 插入变量', viewOnly: '只读', showRunHistory: '显示运行历史', + enableJinja: '开启支持 Jinja 模板', + learnMore: '了解更多', copy: '拷贝', duplicate: '复制', addBlock: '添加节点',