feat: llm support jinja fe (#4260)
This commit is contained in:
@@ -3,33 +3,28 @@ import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import type { Props as EditorProps } from '.'
|
||||
import Editor from '.'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import type { Variable } from '@/app/components/workflow/types'
|
||||
import type { NodeOutPutVar, Variable } from '@/app/components/workflow/types'
|
||||
|
||||
const TO_WINDOW_OFFSET = 8
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
availableVars: NodeOutPutVar[]
|
||||
varList: Variable[]
|
||||
onAddVar: (payload: Variable) => void
|
||||
onAddVar?: (payload: Variable) => void
|
||||
} & EditorProps
|
||||
|
||||
const CodeEditor: FC<Props> = ({
|
||||
nodeId,
|
||||
availableVars,
|
||||
varList,
|
||||
onAddVar,
|
||||
...editorProps
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { availableVars } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
})
|
||||
|
||||
const isLeftBraceRef = useRef(false)
|
||||
|
||||
const editorRef = useRef(null)
|
||||
@@ -76,7 +71,8 @@ const CodeEditor: FC<Props> = ({
|
||||
if (popupPosition.y + height > window.innerHeight - TO_WINDOW_OFFSET)
|
||||
newPopupPosition.y = window.innerHeight - height - TO_WINDOW_OFFSET
|
||||
|
||||
setPopupPosition(newPopupPosition)
|
||||
if (newPopupPosition.x !== popupPosition.x || newPopupPosition.y !== popupPosition.y)
|
||||
setPopupPosition(newPopupPosition)
|
||||
}
|
||||
}, [isShowVarPicker, popupPosition])
|
||||
|
||||
@@ -124,7 +120,7 @@ const CodeEditor: FC<Props> = ({
|
||||
value_selector: varValue,
|
||||
}
|
||||
|
||||
onAddVar(newVar)
|
||||
onAddVar?.(newVar)
|
||||
}
|
||||
const editor: any = editorRef.current
|
||||
const monaco: any = monacoRef.current
|
||||
@@ -143,7 +139,7 @@ const CodeEditor: FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(editorProps.isExpand && 'h-full')}>
|
||||
<Editor
|
||||
{...editorProps}
|
||||
onMount={onEditorMounted}
|
||||
|
@@ -1,20 +1,23 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import Editor, { loader } from '@monaco-editor/react'
|
||||
|
||||
import React, { useRef } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Base from '../base'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
|
||||
import './style.css'
|
||||
|
||||
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
|
||||
loader.config({ paths: { vs: '/vs' } })
|
||||
|
||||
const CODE_EDITOR_LINE_HEIGHT = 18
|
||||
|
||||
export type Props = {
|
||||
value?: string | object
|
||||
placeholder?: string
|
||||
onChange?: (value: string) => void
|
||||
title: JSX.Element
|
||||
title?: JSX.Element
|
||||
language: CodeLanguage
|
||||
headerRight?: JSX.Element
|
||||
readOnly?: boolean
|
||||
@@ -22,6 +25,8 @@ export type Props = {
|
||||
height?: number
|
||||
isInNode?: boolean
|
||||
onMount?: (editor: any, monaco: any) => void
|
||||
noWrapper?: boolean
|
||||
isExpand?: boolean
|
||||
}
|
||||
|
||||
const languageMap = {
|
||||
@@ -30,11 +35,20 @@ const languageMap = {
|
||||
[CodeLanguage.json]: 'json',
|
||||
}
|
||||
|
||||
const DEFAULT_THEME = {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#F2F4F7', // #00000000 transparent. But it will has a blue border
|
||||
},
|
||||
}
|
||||
|
||||
const CodeEditor: FC<Props> = ({
|
||||
value = '',
|
||||
placeholder = '',
|
||||
onChange = () => { },
|
||||
title,
|
||||
title = '',
|
||||
headerRight,
|
||||
language,
|
||||
readOnly,
|
||||
@@ -42,16 +56,37 @@ const CodeEditor: FC<Props> = ({
|
||||
height,
|
||||
isInNode,
|
||||
onMount,
|
||||
noWrapper,
|
||||
isExpand,
|
||||
}) => {
|
||||
const [isFocus, setIsFocus] = React.useState(false)
|
||||
const [isMounted, setIsMounted] = React.useState(false)
|
||||
const minHeight = height || 200
|
||||
const [editorContentHeight, setEditorContentHeight] = useState(56)
|
||||
|
||||
const valueRef = useRef(value)
|
||||
useEffect(() => {
|
||||
valueRef.current = value
|
||||
}, [value])
|
||||
|
||||
const editorRef = useRef<any>(null)
|
||||
const resizeEditorToContent = () => {
|
||||
if (editorRef.current) {
|
||||
const contentHeight = editorRef.current.getContentHeight() // Math.max(, minHeight)
|
||||
setEditorContentHeight(contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditorChange = (value: string | undefined) => {
|
||||
onChange(value || '')
|
||||
setTimeout(() => {
|
||||
resizeEditorToContent()
|
||||
}, 10)
|
||||
}
|
||||
|
||||
const editorRef = useRef(null)
|
||||
const handleEditorDidMount = (editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
resizeEditorToContent()
|
||||
|
||||
editor.onDidFocusEditorText(() => {
|
||||
setIsFocus(true)
|
||||
@@ -60,6 +95,8 @@ const CodeEditor: FC<Props> = ({
|
||||
setIsFocus(false)
|
||||
})
|
||||
|
||||
monaco.editor.defineTheme('default-theme', DEFAULT_THEME)
|
||||
|
||||
monaco.editor.defineTheme('blur-theme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
@@ -78,7 +115,10 @@ const CodeEditor: FC<Props> = ({
|
||||
},
|
||||
})
|
||||
|
||||
monaco.editor.setTheme('default-theme') // Fix: sometimes not load the default theme
|
||||
|
||||
onMount?.(editor, monaco)
|
||||
setIsMounted(true)
|
||||
}
|
||||
|
||||
const outPutValue = (() => {
|
||||
@@ -92,43 +132,63 @@ const CodeEditor: FC<Props> = ({
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Base
|
||||
className='relative'
|
||||
title={title}
|
||||
const theme = (() => {
|
||||
if (noWrapper)
|
||||
return 'default-theme'
|
||||
|
||||
return isFocus ? 'focus-theme' : 'blur-theme'
|
||||
})()
|
||||
|
||||
const main = (
|
||||
<>
|
||||
{/* https://www.npmjs.com/package/@monaco-editor/react */}
|
||||
<Editor
|
||||
// className='min-h-[100%]' // h-full
|
||||
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
|
||||
language={languageMap[language] || 'javascript'}
|
||||
theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme
|
||||
value={outPutValue}
|
||||
headerRight={headerRight}
|
||||
isFocus={isFocus && !readOnly}
|
||||
minHeight={height || 200}
|
||||
isInNode={isInNode}
|
||||
>
|
||||
<>
|
||||
{/* https://www.npmjs.com/package/@monaco-editor/react */}
|
||||
<Editor
|
||||
className='h-full'
|
||||
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
|
||||
language={languageMap[language] || 'javascript'}
|
||||
theme={isFocus ? 'focus-theme' : 'blur-theme'}
|
||||
onChange={handleEditorChange}
|
||||
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
|
||||
options={{
|
||||
readOnly,
|
||||
domReadOnly: true,
|
||||
quickSuggestions: false,
|
||||
minimap: { enabled: false },
|
||||
lineNumbersMinChars: 1, // would change line num width
|
||||
wordWrap: 'on', // auto line wrap
|
||||
// lineNumbers: (num) => {
|
||||
// return <div>{num}</div>
|
||||
// }
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
{!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn(isExpand && 'h-full')}>
|
||||
{noWrapper
|
||||
? <div className='relative no-wrapper' style={{
|
||||
height: isExpand ? '100%' : (editorContentHeight) / 2 + CODE_EDITOR_LINE_HEIGHT, // In IDE, the last line can always be in lop line. So there is some blank space in the bottom.
|
||||
minHeight: CODE_EDITOR_LINE_HEIGHT,
|
||||
}}>
|
||||
{main}
|
||||
</div>
|
||||
: (
|
||||
<Base
|
||||
className='relative'
|
||||
title={title}
|
||||
value={outPutValue}
|
||||
onChange={handleEditorChange}
|
||||
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
|
||||
options={{
|
||||
readOnly,
|
||||
domReadOnly: true,
|
||||
quickSuggestions: false,
|
||||
minimap: { enabled: false },
|
||||
lineNumbersMinChars: 1, // would change line num width
|
||||
wordWrap: 'on', // auto line wrap
|
||||
// lineNumbers: (num) => {
|
||||
// return <div>{num}</div>
|
||||
// }
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
{!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
|
||||
</>
|
||||
</Base>
|
||||
headerRight={headerRight}
|
||||
isFocus={isFocus && !readOnly}
|
||||
minHeight={minHeight}
|
||||
isInNode={isInNode}
|
||||
>
|
||||
{main}
|
||||
</Base>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -2,6 +2,10 @@
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.no-wrapper .margin-view-overlays {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* hide readonly tooltip */
|
||||
.monaco-editor-overlaymessage {
|
||||
display: none !important;
|
||||
|
@@ -1,16 +1,19 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import cn from 'classnames'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
BlockEnum,
|
||||
type Node,
|
||||
type NodeOutPutVar,
|
||||
import { BlockEnum, EditionType } from '../../../../types'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
Variable,
|
||||
} from '../../../../types'
|
||||
|
||||
import Wrap from '../editor/wrap'
|
||||
import { CodeLanguage } from '../../../code/types'
|
||||
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
|
||||
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
@@ -21,6 +24,10 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
headerClassName?: string
|
||||
@@ -42,6 +49,12 @@ type Props = {
|
||||
}
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
// for jinja
|
||||
isSupportJinja?: boolean
|
||||
editionType?: EditionType
|
||||
onEditionTypeChange?: (editionType: EditionType) => void
|
||||
varList?: Variable[]
|
||||
handleAddVariable?: (payload: any) => void
|
||||
}
|
||||
|
||||
const Editor: FC<Props> = ({
|
||||
@@ -61,6 +74,11 @@ const Editor: FC<Props> = ({
|
||||
hasSetBlockStatus,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
isSupportJinja,
|
||||
editionType,
|
||||
onEditionTypeChange,
|
||||
varList = [],
|
||||
handleAddVariable,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@@ -85,20 +103,6 @@ const Editor: FC<Props> = ({
|
||||
setTrue: setFocus,
|
||||
setFalse: setBlur,
|
||||
}] = useBoolean(false)
|
||||
const hideTooltipRunId = useRef(0)
|
||||
|
||||
const [isShowInsertToolTip, setIsShowInsertTooltip] = useState(false)
|
||||
useEffect(() => {
|
||||
if (isFocus) {
|
||||
clearTimeout(hideTooltipRunId.current)
|
||||
setIsShowInsertTooltip(true)
|
||||
}
|
||||
else {
|
||||
hideTooltipRunId.current = setTimeout(() => {
|
||||
setIsShowInsertTooltip(false)
|
||||
}, 100) as any
|
||||
}
|
||||
}, [isFocus])
|
||||
|
||||
const handleInsertVariable = () => {
|
||||
setFocus()
|
||||
@@ -116,6 +120,29 @@ const Editor: FC<Props> = ({
|
||||
<div className='w-px h-3 ml-2 mr-2 bg-gray-200'></div>
|
||||
{/* Operations */}
|
||||
<div className='flex items-center space-x-2'>
|
||||
{isSupportJinja && (
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
<div>
|
||||
<div>{t('workflow.common.enableJinja')}</div>
|
||||
<a className='text-[#155EEF]' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a>
|
||||
</div>
|
||||
}
|
||||
hideArrow
|
||||
>
|
||||
<div className={cn(editionType === EditionType.jinja2 && 'border-black/5 bg-white', 'mb-1 flex h-[22px] items-center px-1.5 rounded-[5px] border border-transparent hover:border-black/5 space-x-0.5')}>
|
||||
<Jinja className='w-6 h-3 text-gray-300' />
|
||||
<Switch
|
||||
size='sm'
|
||||
defaultValue={editionType === EditionType.jinja2}
|
||||
onChange={(checked) => {
|
||||
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
|
||||
)}
|
||||
{!readOnly && (
|
||||
<TooltipPlus
|
||||
popupContent={`${t('workflow.common.insertVarTip')}`}
|
||||
@@ -142,57 +169,75 @@ const Editor: FC<Props> = ({
|
||||
|
||||
{/* Min: 80 Max: 560. Header: 24 */}
|
||||
<div className={cn('pb-2', isExpand && 'flex flex-col grow')}>
|
||||
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
|
||||
<PromptEditor
|
||||
instanceId={instanceId}
|
||||
compact
|
||||
className='min-h-[56px]'
|
||||
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
|
||||
value={value}
|
||||
contextBlock={{
|
||||
show: justVar ? false : isShowContext,
|
||||
selectable: !hasSetBlockStatus?.context,
|
||||
canNotAddContext: true,
|
||||
}}
|
||||
historyBlock={{
|
||||
show: justVar ? false : isShowHistory,
|
||||
selectable: !hasSetBlockStatus?.history,
|
||||
history: {
|
||||
user: 'Human',
|
||||
assistant: 'Assistant',
|
||||
},
|
||||
}}
|
||||
queryBlock={{
|
||||
show: false, // use [sys.query] instead of query block
|
||||
selectable: false,
|
||||
}}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
onChange={onChange}
|
||||
onBlur={setBlur}
|
||||
onFocus={setFocus}
|
||||
editable={!readOnly}
|
||||
/>
|
||||
{/* to patch Editor not support dynamic change editable status */}
|
||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
||||
</div>
|
||||
{!(isSupportJinja && editionType === EditionType.jinja2)
|
||||
? (
|
||||
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
|
||||
<PromptEditor
|
||||
instanceId={instanceId}
|
||||
compact
|
||||
className='min-h-[56px]'
|
||||
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
|
||||
value={value}
|
||||
contextBlock={{
|
||||
show: justVar ? false : isShowContext,
|
||||
selectable: !hasSetBlockStatus?.context,
|
||||
canNotAddContext: true,
|
||||
}}
|
||||
historyBlock={{
|
||||
show: justVar ? false : isShowHistory,
|
||||
selectable: !hasSetBlockStatus?.history,
|
||||
history: {
|
||||
user: 'Human',
|
||||
assistant: 'Assistant',
|
||||
},
|
||||
}}
|
||||
queryBlock={{
|
||||
show: false, // use [sys.query] instead of query block
|
||||
selectable: false,
|
||||
}}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
onChange={onChange}
|
||||
onBlur={setBlur}
|
||||
onFocus={setFocus}
|
||||
editable={!readOnly}
|
||||
/>
|
||||
{/* to patch Editor not support dynamic change editable status */}
|
||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
|
||||
<CodeEditor
|
||||
availableVars={nodesOutputVars || []}
|
||||
varList={varList}
|
||||
onAddVar={handleAddVariable}
|
||||
isInNode
|
||||
readOnly={readOnly}
|
||||
language={CodeLanguage.python3}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
noWrapper
|
||||
isExpand={isExpand}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Wrap>
|
||||
|
Reference in New Issue
Block a user