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
21 changed files with 621 additions and 177 deletions

View File

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

View File

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

View File

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

View File

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