mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-15 22:06:52 +08:00
FEAT: NEW WORKFLOW ENGINE (#3160)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yeuoly <admin@srmxy.cn> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { ValueSelector } from '../../workflow/types'
|
||||
|
||||
export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}'
|
||||
export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}'
|
||||
export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}'
|
||||
@@ -22,3 +24,25 @@ export const checkHasQueryBlock = (text: string) => {
|
||||
return false
|
||||
return text.includes(QUERY_PLACEHOLDER_TEXT)
|
||||
}
|
||||
|
||||
/*
|
||||
* {{#1711617514996.name#}} => [1711617514996, name]
|
||||
* {{#1711617514996.sys.query#}} => [sys, query]
|
||||
*/
|
||||
export const getInputVars = (text: string): ValueSelector[] => {
|
||||
const allVars = text.match(/{{#([^#]*)#}}/g)
|
||||
if (allVars && allVars?.length > 0) {
|
||||
// {{#context#}}, {{#query#}} is not input vars
|
||||
const inputVars = allVars
|
||||
.filter(item => item.includes('.'))
|
||||
.map((item) => {
|
||||
const valueSelector = item.replace('{{#', '').replace('#}}', '').split('.')
|
||||
if (valueSelector[1] === 'sys' && /^\d+$/.test(valueSelector[0]))
|
||||
return valueSelector.slice(1)
|
||||
|
||||
return valueSelector
|
||||
})
|
||||
return inputVars
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -158,9 +158,9 @@ export function useBasicTypeaheadTriggerMatch(
|
||||
): TriggerFn {
|
||||
return useCallback(
|
||||
(text: string) => {
|
||||
const validChars = `[^${trigger}${PUNCTUATION}\\s]`
|
||||
const validChars = `[${PUNCTUATION}\\s]`
|
||||
const TypeaheadTriggerRegex = new RegExp(
|
||||
`([^${trigger}]|^)(`
|
||||
'(.*)('
|
||||
+ `[${trigger}]`
|
||||
+ `((?:${validChars}){0,${maxLength}})`
|
||||
+ ')$',
|
||||
|
||||
@@ -17,27 +17,42 @@ import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
|
||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
|
||||
// import TreeView from './plugins/tree-view'
|
||||
import Placeholder from './plugins/placeholder'
|
||||
import ComponentPicker from './plugins/component-picker'
|
||||
import VariablePicker from './plugins/variable-picker'
|
||||
import ContextBlock from './plugins/context-block'
|
||||
import { ContextBlockNode } from './plugins/context-block/node'
|
||||
import ContextBlockReplacementBlock from './plugins/context-block-replacement-block'
|
||||
import HistoryBlock from './plugins/history-block'
|
||||
import { HistoryBlockNode } from './plugins/history-block/node'
|
||||
import HistoryBlockReplacementBlock from './plugins/history-block-replacement-block'
|
||||
import QueryBlock from './plugins/query-block'
|
||||
import { QueryBlockNode } from './plugins/query-block/node'
|
||||
import QueryBlockReplacementBlock from './plugins/query-block-replacement-block'
|
||||
import ComponentPickerBlock from './plugins/component-picker-block'
|
||||
import {
|
||||
ContextBlock,
|
||||
ContextBlockNode,
|
||||
ContextBlockReplacementBlock,
|
||||
} from './plugins/context-block'
|
||||
import {
|
||||
QueryBlock,
|
||||
QueryBlockNode,
|
||||
QueryBlockReplacementBlock,
|
||||
} from './plugins/query-block'
|
||||
import {
|
||||
HistoryBlock,
|
||||
HistoryBlockNode,
|
||||
HistoryBlockReplacementBlock,
|
||||
} from './plugins/history-block'
|
||||
import {
|
||||
WorkflowVariableBlock,
|
||||
WorkflowVariableBlockNode,
|
||||
WorkflowVariableBlockReplacementBlock,
|
||||
} from './plugins/workflow-variable-block'
|
||||
import VariableBlock from './plugins/variable-block'
|
||||
import VariableValueBlock from './plugins/variable-value-block'
|
||||
import { VariableValueBlockNode } from './plugins/variable-value-block/node'
|
||||
import { CustomTextNode } from './plugins/custom-text/node'
|
||||
import OnBlurBlock from './plugins/on-blur-block'
|
||||
import OnBlurBlock from './plugins/on-blur-or-focus-block'
|
||||
import UpdateBlock from './plugins/update-block'
|
||||
import { textToEditorState } from './utils'
|
||||
import type { Dataset } from './plugins/context-block'
|
||||
import type { RoleName } from './plugins/history-block'
|
||||
import type { ExternalToolOption, Option } from './plugins/variable-picker'
|
||||
import type {
|
||||
ContextBlockType,
|
||||
ExternalToolBlockType,
|
||||
HistoryBlockType,
|
||||
QueryBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
} from './types'
|
||||
import {
|
||||
UPDATE_DATASETS_EVENT_EMITTER,
|
||||
UPDATE_HISTORY_EVENT_EMITTER,
|
||||
@@ -45,75 +60,43 @@ import {
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
export type PromptEditorProps = {
|
||||
instanceId?: string
|
||||
compact?: boolean
|
||||
className?: string
|
||||
placeholder?: string
|
||||
placeholderClassName?: string
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
editable?: boolean
|
||||
onChange?: (text: string) => void
|
||||
onBlur?: () => void
|
||||
contextBlock?: {
|
||||
show?: boolean
|
||||
selectable?: boolean
|
||||
datasets: Dataset[]
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
onAddContext: () => void
|
||||
}
|
||||
variableBlock?: {
|
||||
selectable?: boolean
|
||||
variables: Option[]
|
||||
externalTools?: ExternalToolOption[]
|
||||
onAddExternalTool?: () => void
|
||||
}
|
||||
historyBlock?: {
|
||||
show?: boolean
|
||||
selectable?: boolean
|
||||
history: RoleName
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
onEditRole: () => void
|
||||
}
|
||||
queryBlock?: {
|
||||
show?: boolean
|
||||
selectable?: boolean
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
onFocus?: () => void
|
||||
contextBlock?: ContextBlockType
|
||||
queryBlock?: QueryBlockType
|
||||
historyBlock?: HistoryBlockType
|
||||
variableBlock?: VariableBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
}
|
||||
|
||||
const PromptEditor: FC<PromptEditorProps> = ({
|
||||
instanceId,
|
||||
compact,
|
||||
className,
|
||||
placeholder,
|
||||
placeholderClassName,
|
||||
style,
|
||||
value,
|
||||
editable = true,
|
||||
onChange,
|
||||
onBlur,
|
||||
contextBlock = {
|
||||
show: true,
|
||||
selectable: true,
|
||||
datasets: [],
|
||||
onAddContext: () => {},
|
||||
onInsert: () => {},
|
||||
onDelete: () => {},
|
||||
},
|
||||
historyBlock = {
|
||||
show: true,
|
||||
selectable: true,
|
||||
history: {
|
||||
user: '',
|
||||
assistant: '',
|
||||
},
|
||||
onEditRole: () => {},
|
||||
onInsert: () => {},
|
||||
onDelete: () => {},
|
||||
},
|
||||
variableBlock = {
|
||||
variables: [],
|
||||
},
|
||||
queryBlock = {
|
||||
show: true,
|
||||
selectable: true,
|
||||
onInsert: () => {},
|
||||
onDelete: () => {},
|
||||
},
|
||||
onFocus,
|
||||
contextBlock,
|
||||
queryBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
}) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const initialConfig = {
|
||||
@@ -128,6 +111,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
ContextBlockNode,
|
||||
HistoryBlockNode,
|
||||
QueryBlockNode,
|
||||
WorkflowVariableBlockNode,
|
||||
VariableValueBlockNode,
|
||||
],
|
||||
editorState: value ? textToEditorState(value as string) : null,
|
||||
@@ -145,87 +129,85 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
useEffect(() => {
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_DATASETS_EVENT_EMITTER,
|
||||
payload: contextBlock.datasets,
|
||||
payload: contextBlock?.datasets,
|
||||
} as any)
|
||||
}, [eventEmitter, contextBlock.datasets])
|
||||
}, [eventEmitter, contextBlock?.datasets])
|
||||
useEffect(() => {
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_HISTORY_EVENT_EMITTER,
|
||||
payload: historyBlock.history,
|
||||
payload: historyBlock?.history,
|
||||
} as any)
|
||||
}, [eventEmitter, historyBlock.history])
|
||||
}, [eventEmitter, historyBlock?.history])
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
||||
<div className='relative'>
|
||||
<RichTextPlugin
|
||||
contentEditable={<ContentEditable className={`${className} outline-none text-sm text-gray-700 leading-6`} />}
|
||||
placeholder={<Placeholder />}
|
||||
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`} style={style || {}} />}
|
||||
placeholder={<Placeholder value={placeholder} className={placeholderClassName} compact={compact} />}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<ComponentPicker
|
||||
contextDisabled={!contextBlock.selectable}
|
||||
contextShow={contextBlock.show}
|
||||
historyDisabled={!historyBlock.selectable}
|
||||
historyShow={historyBlock.show}
|
||||
queryDisabled={!queryBlock.selectable}
|
||||
queryShow={queryBlock.show}
|
||||
<ComponentPickerBlock
|
||||
triggerString='/'
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
/>
|
||||
<VariablePicker
|
||||
items={variableBlock.variables}
|
||||
externalTools={variableBlock.externalTools}
|
||||
onAddExternalTool={variableBlock.onAddExternalTool}
|
||||
<ComponentPickerBlock
|
||||
triggerString='{'
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
/>
|
||||
{
|
||||
contextBlock.show && (
|
||||
contextBlock?.show && (
|
||||
<>
|
||||
<ContextBlock
|
||||
datasets={contextBlock.datasets}
|
||||
onAddContext={contextBlock.onAddContext}
|
||||
onInsert={contextBlock.onInsert}
|
||||
onDelete={contextBlock.onDelete}
|
||||
/>
|
||||
<ContextBlockReplacementBlock
|
||||
datasets={contextBlock.datasets}
|
||||
onAddContext={contextBlock.onAddContext}
|
||||
onInsert={contextBlock.onInsert}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<VariableBlock />
|
||||
{
|
||||
historyBlock.show && (
|
||||
<>
|
||||
<HistoryBlock
|
||||
roleName={historyBlock.history}
|
||||
onEditRole={historyBlock.onEditRole}
|
||||
onInsert={historyBlock.onInsert}
|
||||
onDelete={historyBlock.onDelete}
|
||||
/>
|
||||
<HistoryBlockReplacementBlock
|
||||
roleName={historyBlock.history}
|
||||
onEditRole={historyBlock.onEditRole}
|
||||
onInsert={historyBlock.onInsert}
|
||||
/>
|
||||
<ContextBlock {...contextBlock} />
|
||||
<ContextBlockReplacementBlock {...contextBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
queryBlock.show && (
|
||||
queryBlock?.show && (
|
||||
<>
|
||||
<QueryBlock
|
||||
onInsert={queryBlock.onInsert}
|
||||
onDelete={queryBlock.onDelete}
|
||||
/>
|
||||
<QueryBlock {...queryBlock} />
|
||||
<QueryBlockReplacementBlock />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<VariableValueBlock />
|
||||
{
|
||||
historyBlock?.show && (
|
||||
<>
|
||||
<HistoryBlock {...historyBlock} />
|
||||
<HistoryBlockReplacementBlock {...historyBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
(variableBlock?.show || externalToolBlock?.show) && (
|
||||
<>
|
||||
<VariableBlock />
|
||||
<VariableValueBlock />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && (
|
||||
<>
|
||||
<WorkflowVariableBlock {...workflowVariableBlock} />
|
||||
<WorkflowVariableBlockReplacementBlock {...workflowVariableBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<OnChangePlugin onChange={handleEditorChange} />
|
||||
<OnBlurBlock onBlur={onBlur} />
|
||||
<UpdateBlock />
|
||||
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
|
||||
<UpdateBlock instanceId={instanceId} />
|
||||
{/* <TreeView /> */}
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { memo } from 'react'
|
||||
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
|
||||
export class VariableOption extends MenuOption {
|
||||
title: string
|
||||
icon?: JSX.Element
|
||||
extraElement?: JSX.Element
|
||||
keywords: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
|
||||
constructor(
|
||||
title: string,
|
||||
options: {
|
||||
icon?: JSX.Element
|
||||
extraElement?: JSX.Element
|
||||
keywords?: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
},
|
||||
) {
|
||||
super(title)
|
||||
this.title = title
|
||||
this.keywords = options.keywords || []
|
||||
this.icon = options.icon
|
||||
this.extraElement = options.extraElement
|
||||
this.keyboardShortcut = options.keyboardShortcut
|
||||
this.onSelect = options.onSelect.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
type VariableMenuItemProps = {
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
onMouseEnter: () => void
|
||||
option: VariableOption
|
||||
queryString: string | null
|
||||
}
|
||||
export const VariableMenuItem = memo(({
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
option,
|
||||
queryString,
|
||||
}: VariableMenuItemProps) => {
|
||||
const title = option.title
|
||||
let before = title
|
||||
let middle = ''
|
||||
let after = ''
|
||||
|
||||
if (queryString) {
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
const match = regex.exec(option.title)
|
||||
|
||||
if (match) {
|
||||
before = title.substring(0, match.index)
|
||||
middle = match[0]
|
||||
after = title.substring(match.index + match[0].length)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
className={`
|
||||
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
|
||||
${isSelected && 'bg-primary-50'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={option.setRefElement}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}>
|
||||
<div className='mr-2'>
|
||||
{option.icon}
|
||||
</div>
|
||||
<div className='grow text-[13px] text-gray-900 truncate' title={option.title}>
|
||||
{before}
|
||||
<span className='text-[#2970FF]'>{middle}</span>
|
||||
{after}
|
||||
</div>
|
||||
{option.extraElement}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
VariableMenuItem.displayName = 'VariableMenuItem'
|
||||
@@ -0,0 +1,201 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { $insertNodes } from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type {
|
||||
ContextBlockType,
|
||||
ExternalToolBlockType,
|
||||
HistoryBlockType,
|
||||
QueryBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
} from '../../types'
|
||||
import { INSERT_CONTEXT_BLOCK_COMMAND } from '../context-block'
|
||||
import { INSERT_HISTORY_BLOCK_COMMAND } from '../history-block'
|
||||
import { INSERT_QUERY_BLOCK_COMMAND } from '../query-block'
|
||||
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
|
||||
import { $createCustomTextNode } from '../custom-text/node'
|
||||
import { PromptOption } from './prompt-option'
|
||||
import { VariableOption } from './variable-option'
|
||||
import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import {
|
||||
MessageClockCircle,
|
||||
Tool03,
|
||||
} from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { BracketsX } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
|
||||
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
export const usePromptOptions = (
|
||||
contextBlock?: ContextBlockType,
|
||||
queryBlock?: QueryBlockType,
|
||||
historyBlock?: HistoryBlockType,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
return useMemo(() => {
|
||||
return [
|
||||
...contextBlock?.show
|
||||
? [
|
||||
new PromptOption(t('common.promptEditor.context.item.title'), {
|
||||
icon: <File05 className='w-4 h-4 text-[#6938EF]' />,
|
||||
onSelect: () => {
|
||||
if (!contextBlock?.selectable)
|
||||
return
|
||||
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
disabled: !contextBlock?.selectable,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
...queryBlock?.show
|
||||
? [
|
||||
new PromptOption(t('common.promptEditor.query.item.title'), {
|
||||
icon: <UserEdit02 className='w-4 h-4 text-[#FD853A]' />,
|
||||
onSelect: () => {
|
||||
if (!queryBlock?.selectable)
|
||||
return
|
||||
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
disabled: !queryBlock?.selectable,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
...historyBlock?.show
|
||||
? [
|
||||
new PromptOption(t('common.promptEditor.history.item.title'), {
|
||||
icon: <MessageClockCircle className='w-4 h-4 text-[#DD2590]' />,
|
||||
onSelect: () => {
|
||||
if (!historyBlock?.selectable)
|
||||
return
|
||||
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
disabled: !historyBlock?.selectable,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
]
|
||||
}, [contextBlock, editor, historyBlock, queryBlock, t])
|
||||
}
|
||||
|
||||
export const useVariableOptions = (
|
||||
variableBlock?: VariableBlockType,
|
||||
queryString?: string,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const options = useMemo(() => {
|
||||
const baseOptions = (variableBlock?.variables || []).map((item) => {
|
||||
return new VariableOption(item.value, {
|
||||
icon: <BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />,
|
||||
onSelect: () => {
|
||||
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
|
||||
},
|
||||
})
|
||||
})
|
||||
if (!queryString)
|
||||
return baseOptions
|
||||
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
|
||||
return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
|
||||
}, [editor, queryString, variableBlock])
|
||||
|
||||
const addOption = useMemo(() => {
|
||||
return new VariableOption(t('common.promptEditor.variable.modal.add'), {
|
||||
icon: <BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />,
|
||||
onSelect: () => {
|
||||
editor.update(() => {
|
||||
const prefixNode = $createCustomTextNode('{{')
|
||||
const suffixNode = $createCustomTextNode('}}')
|
||||
$insertNodes([prefixNode, suffixNode])
|
||||
prefixNode.select()
|
||||
})
|
||||
},
|
||||
})
|
||||
}, [editor, t])
|
||||
|
||||
return useMemo(() => {
|
||||
return variableBlock?.show ? [...options, addOption] : []
|
||||
}, [options, addOption, variableBlock?.show])
|
||||
}
|
||||
|
||||
export const useExternalToolOptions = (
|
||||
externalToolBlockType?: ExternalToolBlockType,
|
||||
queryString?: string,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const options = useMemo(() => {
|
||||
const baseToolOptions = (externalToolBlockType?.externalTools || []).map((item) => {
|
||||
return new VariableOption(item.name, {
|
||||
icon: (
|
||||
<AppIcon
|
||||
className='!w-[14px] !h-[14px]'
|
||||
icon={item.icon}
|
||||
background={item.icon_background}
|
||||
/>
|
||||
),
|
||||
extraElement: <div className='text-xs text-gray-400'>{item.variableName}</div>,
|
||||
onSelect: () => {
|
||||
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
|
||||
},
|
||||
})
|
||||
})
|
||||
if (!queryString)
|
||||
return baseToolOptions
|
||||
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
|
||||
return baseToolOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
|
||||
}, [editor, queryString, externalToolBlockType])
|
||||
|
||||
const addOption = useMemo(() => {
|
||||
return new VariableOption(t('common.promptEditor.variable.modal.addTool'), {
|
||||
icon: <Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />,
|
||||
extraElement: <ArrowUpRight className='w-3 h-3 text-gray-400' />,
|
||||
onSelect: () => {
|
||||
if (externalToolBlockType?.onAddExternalTool)
|
||||
externalToolBlockType.onAddExternalTool()
|
||||
},
|
||||
})
|
||||
}, [externalToolBlockType, t])
|
||||
|
||||
return useMemo(() => {
|
||||
return externalToolBlockType?.show ? [...options, addOption] : []
|
||||
}, [options, addOption, externalToolBlockType?.show])
|
||||
}
|
||||
|
||||
export const useOptions = (
|
||||
contextBlock?: ContextBlockType,
|
||||
queryBlock?: QueryBlockType,
|
||||
historyBlock?: HistoryBlockType,
|
||||
variableBlock?: VariableBlockType,
|
||||
externalToolBlockType?: ExternalToolBlockType,
|
||||
workflowVariableBlockType?: WorkflowVariableBlockType,
|
||||
queryString?: string,
|
||||
) => {
|
||||
const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock)
|
||||
const variableOptions = useVariableOptions(variableBlock, queryString)
|
||||
const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString)
|
||||
const workflowVariableOptions = useMemo(() => {
|
||||
if (!workflowVariableBlockType?.show)
|
||||
return []
|
||||
|
||||
return workflowVariableBlockType.variables || []
|
||||
}, [workflowVariableBlockType])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
promptOptions,
|
||||
variableOptions,
|
||||
externalToolOptions,
|
||||
workflowVariableOptions,
|
||||
allOptions: [...promptOptions, ...variableOptions, ...externalToolOptions],
|
||||
}
|
||||
}, [promptOptions, variableOptions, externalToolOptions, workflowVariableOptions])
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {
|
||||
FloatingPortal,
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
useFloating,
|
||||
} from '@floating-ui/react'
|
||||
import type { TextNode } from 'lexical'
|
||||
import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import type {
|
||||
ContextBlockType,
|
||||
ExternalToolBlockType,
|
||||
HistoryBlockType,
|
||||
QueryBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
} from '../../types'
|
||||
import { useBasicTypeaheadTriggerMatch } from '../../hooks'
|
||||
import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block'
|
||||
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
|
||||
import { $splitNodeContainingQuery } from '../../utils'
|
||||
import type { PromptOption } from './prompt-option'
|
||||
import PromptMenu from './prompt-menu'
|
||||
import VariableMenu from './variable-menu'
|
||||
import type { VariableOption } from './variable-option'
|
||||
import { useOptions } from './hooks'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
type ComponentPickerProps = {
|
||||
triggerString: string
|
||||
contextBlock?: ContextBlockType
|
||||
queryBlock?: QueryBlockType
|
||||
historyBlock?: HistoryBlockType
|
||||
variableBlock?: VariableBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
}
|
||||
const ComponentPicker = ({
|
||||
triggerString,
|
||||
contextBlock,
|
||||
queryBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
}: ComponentPickerProps) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const { refs, floatingStyles, elements } = useFloating({
|
||||
placement: 'bottom-start',
|
||||
middleware: [
|
||||
offset(16), // fix hide cursor
|
||||
shift(),
|
||||
flip(),
|
||||
],
|
||||
})
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, {
|
||||
minLength: 0,
|
||||
maxLength: 0,
|
||||
})
|
||||
|
||||
const [queryString, setQueryString] = useState<string | null>(null)
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
|
||||
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
|
||||
})
|
||||
|
||||
const {
|
||||
allOptions,
|
||||
promptOptions,
|
||||
variableOptions,
|
||||
externalToolOptions,
|
||||
workflowVariableOptions,
|
||||
} = useOptions(
|
||||
contextBlock,
|
||||
queryBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
)
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(
|
||||
selectedOption: PromptOption | VariableOption,
|
||||
nodeToRemove: TextNode | null,
|
||||
closeMenu: () => void,
|
||||
matchingString: string,
|
||||
) => {
|
||||
editor.update(() => {
|
||||
if (nodeToRemove && selectedOption?.key)
|
||||
nodeToRemove.remove()
|
||||
|
||||
if (selectedOption?.onSelect)
|
||||
selectedOption.onSelect(matchingString)
|
||||
|
||||
closeMenu()
|
||||
})
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
|
||||
const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
|
||||
editor.update(() => {
|
||||
const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
|
||||
if (needRemove)
|
||||
needRemove.remove()
|
||||
})
|
||||
|
||||
if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
|
||||
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
|
||||
else
|
||||
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
|
||||
}, [editor, checkForTriggerMatch, triggerString])
|
||||
|
||||
const renderMenu = useCallback<MenuRenderFn<PromptOption | VariableOption>>((
|
||||
anchorElementRef,
|
||||
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
||||
) => {
|
||||
if (anchorElementRef.current && (allOptions.length || workflowVariableBlock?.show)) {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
ReactDOM.createPortal(
|
||||
<div ref={refs.setReference}></div>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
}
|
||||
{
|
||||
elements.reference && (
|
||||
<FloatingPortal id='typeahead-menu'>
|
||||
<div
|
||||
className='w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg overflow-y-auto'
|
||||
style={{
|
||||
...floatingStyles,
|
||||
maxHeight: 'calc(1 / 3 * 100vh)',
|
||||
}}
|
||||
ref={refs.setFloating}
|
||||
>
|
||||
{
|
||||
!!promptOptions.length && (
|
||||
<>
|
||||
<PromptMenu
|
||||
startIndex={0}
|
||||
selectedIndex={selectedIndex}
|
||||
options={promptOptions}
|
||||
onClick={(index, option) => {
|
||||
if (option.disabled)
|
||||
return
|
||||
setHighlightedIndex(index)
|
||||
selectOptionAndCleanUp(option)
|
||||
}}
|
||||
onMouseEnter={(index, option) => {
|
||||
if (option.disabled)
|
||||
return
|
||||
setHighlightedIndex(index)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!variableOptions.length && (
|
||||
<>
|
||||
{
|
||||
!!promptOptions.length && (
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
)
|
||||
}
|
||||
<VariableMenu
|
||||
startIndex={promptOptions.length}
|
||||
selectedIndex={selectedIndex}
|
||||
options={variableOptions}
|
||||
onClick={(index, option) => {
|
||||
if (option.disabled)
|
||||
return
|
||||
setHighlightedIndex(index)
|
||||
selectOptionAndCleanUp(option)
|
||||
}}
|
||||
onMouseEnter={(index, option) => {
|
||||
if (option.disabled)
|
||||
return
|
||||
setHighlightedIndex(index)
|
||||
}}
|
||||
queryString={queryString}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!externalToolOptions.length && (
|
||||
<>
|
||||
{
|
||||
(!!promptOptions.length || !!variableOptions.length) && (
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
)
|
||||
}
|
||||
<VariableMenu
|
||||
startIndex={promptOptions.length + variableOptions.length}
|
||||
selectedIndex={selectedIndex}
|
||||
options={externalToolOptions}
|
||||
onClick={(index, option) => {
|
||||
if (option.disabled)
|
||||
return
|
||||
setHighlightedIndex(index)
|
||||
selectOptionAndCleanUp(option)
|
||||
}}
|
||||
onMouseEnter={(index, option) => {
|
||||
if (option.disabled)
|
||||
return
|
||||
setHighlightedIndex(index)
|
||||
}}
|
||||
queryString={queryString}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && (
|
||||
<>
|
||||
{
|
||||
(!!promptOptions.length || !!variableOptions.length || !!externalToolOptions.length) && (
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
)
|
||||
}
|
||||
<div className='p-1'>
|
||||
<VarReferenceVars
|
||||
hideSearch
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</FloatingPortal>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [
|
||||
allOptions,
|
||||
promptOptions,
|
||||
variableOptions,
|
||||
externalToolOptions,
|
||||
queryString,
|
||||
workflowVariableBlock?.show,
|
||||
workflowVariableOptions,
|
||||
handleSelectWorkflowVariable,
|
||||
elements,
|
||||
floatingStyles,
|
||||
refs,
|
||||
])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
options={allOptions as any}
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
anchorClassName='z-[999999]'
|
||||
menuRenderFn={renderMenu}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ComponentPicker)
|
||||
@@ -0,0 +1,37 @@
|
||||
import { memo } from 'react'
|
||||
import { PromptMenuItem } from './prompt-option'
|
||||
|
||||
type PromptMenuProps = {
|
||||
startIndex: number
|
||||
selectedIndex: number | null
|
||||
options: any[]
|
||||
onClick: (index: number, option: any) => void
|
||||
onMouseEnter: (index: number, option: any) => void
|
||||
}
|
||||
const PromptMenu = ({
|
||||
startIndex,
|
||||
selectedIndex,
|
||||
options,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
}: PromptMenuProps) => {
|
||||
return (
|
||||
<div className='p-1'>
|
||||
{
|
||||
options.map((option, index: number) => (
|
||||
<PromptMenuItem
|
||||
startIndex={startIndex}
|
||||
index={index}
|
||||
isSelected={selectedIndex === index + startIndex}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
key={option.key}
|
||||
option={option}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PromptMenu)
|
||||
@@ -0,0 +1,65 @@
|
||||
import { memo } from 'react'
|
||||
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
|
||||
export class PromptOption extends MenuOption {
|
||||
title: string
|
||||
icon?: JSX.Element
|
||||
keywords: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
disabled?: boolean
|
||||
|
||||
constructor(
|
||||
title: string,
|
||||
options: {
|
||||
icon?: JSX.Element
|
||||
keywords?: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
disabled?: boolean
|
||||
},
|
||||
) {
|
||||
super(title)
|
||||
this.title = title
|
||||
this.keywords = options.keywords || []
|
||||
this.icon = options.icon
|
||||
this.keyboardShortcut = options.keyboardShortcut
|
||||
this.onSelect = options.onSelect.bind(this)
|
||||
this.disabled = options.disabled
|
||||
}
|
||||
}
|
||||
|
||||
type PromptMenuItemMenuItemProps = {
|
||||
startIndex: number
|
||||
index: number
|
||||
isSelected: boolean
|
||||
onClick: (index: number, option: PromptOption) => void
|
||||
onMouseEnter: (index: number, option: PromptOption) => void
|
||||
option: PromptOption
|
||||
}
|
||||
export const PromptMenuItem = memo(({
|
||||
startIndex,
|
||||
index,
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
option,
|
||||
}: PromptMenuItemMenuItemProps) => {
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
className={`
|
||||
flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50 rounded-md
|
||||
${isSelected && !option.disabled && '!bg-gray-50'}
|
||||
${option.disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={option.setRefElement}
|
||||
onMouseEnter={() => onMouseEnter(index + startIndex, option)}
|
||||
onClick={() => onClick(index + startIndex, option)}>
|
||||
{option.icon}
|
||||
<div className='ml-1 text-[13px] text-gray-900'>{option.title}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
PromptMenuItem.displayName = 'PromptMenuItem'
|
||||
@@ -0,0 +1,40 @@
|
||||
import { memo } from 'react'
|
||||
import { VariableMenuItem } from './variable-option'
|
||||
|
||||
type VariableMenuProps = {
|
||||
startIndex: number
|
||||
selectedIndex: number | null
|
||||
options: any[]
|
||||
onClick: (index: number, option: any) => void
|
||||
onMouseEnter: (index: number, option: any) => void
|
||||
queryString: string | null
|
||||
}
|
||||
const VariableMenu = ({
|
||||
startIndex,
|
||||
selectedIndex,
|
||||
options,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
queryString,
|
||||
}: VariableMenuProps) => {
|
||||
return (
|
||||
<div className='p-1'>
|
||||
{
|
||||
options.map((option, index: number) => (
|
||||
<VariableMenuItem
|
||||
startIndex={startIndex}
|
||||
index={index}
|
||||
isSelected={selectedIndex === index + startIndex}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
key={option.key}
|
||||
option={option}
|
||||
queryString={queryString}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(VariableMenu)
|
||||
@@ -0,0 +1,89 @@
|
||||
import { memo } from 'react'
|
||||
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
|
||||
export class VariableOption extends MenuOption {
|
||||
title: string
|
||||
icon?: JSX.Element
|
||||
extraElement?: JSX.Element
|
||||
keywords: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
|
||||
constructor(
|
||||
title: string,
|
||||
options: {
|
||||
icon?: JSX.Element
|
||||
extraElement?: JSX.Element
|
||||
keywords?: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
},
|
||||
) {
|
||||
super(title)
|
||||
this.title = title
|
||||
this.keywords = options.keywords || []
|
||||
this.icon = options.icon
|
||||
this.extraElement = options.extraElement
|
||||
this.keyboardShortcut = options.keyboardShortcut
|
||||
this.onSelect = options.onSelect.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
type VariableMenuItemProps = {
|
||||
startIndex: number
|
||||
index: number
|
||||
isSelected: boolean
|
||||
onClick: (index: number, option: VariableOption) => void
|
||||
onMouseEnter: (index: number, option: VariableOption) => void
|
||||
option: VariableOption
|
||||
queryString: string | null
|
||||
}
|
||||
export const VariableMenuItem = memo(({
|
||||
startIndex,
|
||||
index,
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
option,
|
||||
queryString,
|
||||
}: VariableMenuItemProps) => {
|
||||
const title = option.title
|
||||
let before = title
|
||||
let middle = ''
|
||||
let after = ''
|
||||
|
||||
if (queryString) {
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
const match = regex.exec(option.title)
|
||||
|
||||
if (match) {
|
||||
before = title.substring(0, match.index)
|
||||
middle = match[0]
|
||||
after = title.substring(match.index + match[0].length)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
className={`
|
||||
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
|
||||
${isSelected && 'bg-primary-50'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={option.setRefElement}
|
||||
onMouseEnter={() => onMouseEnter(index + startIndex, option)}
|
||||
onClick={() => onClick(index + startIndex, option)}>
|
||||
<div className='mr-2'>
|
||||
{option.icon}
|
||||
</div>
|
||||
<div className='grow text-[13px] text-gray-900 truncate' title={option.title}>
|
||||
{before}
|
||||
<span className='text-[#2970FF]'>{middle}</span>
|
||||
{after}
|
||||
</div>
|
||||
{option.extraElement}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
VariableMenuItem.displayName = 'VariableMenuItem'
|
||||
@@ -1,225 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { TextNode } from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import {
|
||||
LexicalTypeaheadMenuPlugin,
|
||||
MenuOption,
|
||||
} from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import { useBasicTypeaheadTriggerMatch } from '../hooks'
|
||||
import { INSERT_CONTEXT_BLOCK_COMMAND } from './context-block'
|
||||
import { INSERT_VARIABLE_BLOCK_COMMAND } from './variable-block'
|
||||
import { INSERT_HISTORY_BLOCK_COMMAND } from './history-block'
|
||||
import { INSERT_QUERY_BLOCK_COMMAND } from './query-block'
|
||||
import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import { Variable } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
|
||||
|
||||
class ComponentPickerOption extends MenuOption {
|
||||
title: string
|
||||
icon?: JSX.Element
|
||||
keywords: Array<string>
|
||||
keyboardShortcut?: string
|
||||
desc: string
|
||||
onSelect: (queryString: string) => void
|
||||
disabled?: boolean
|
||||
|
||||
constructor(
|
||||
title: string,
|
||||
options: {
|
||||
icon?: JSX.Element
|
||||
keywords?: Array<string>
|
||||
keyboardShortcut?: string
|
||||
desc: string
|
||||
onSelect: (queryString: string) => void
|
||||
disabled?: boolean
|
||||
},
|
||||
) {
|
||||
super(title)
|
||||
this.title = title
|
||||
this.keywords = options.keywords || []
|
||||
this.icon = options.icon
|
||||
this.keyboardShortcut = options.keyboardShortcut
|
||||
this.desc = options.desc
|
||||
this.onSelect = options.onSelect.bind(this)
|
||||
this.disabled = options.disabled
|
||||
}
|
||||
}
|
||||
|
||||
type ComponentPickerMenuItemProps = {
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
onMouseEnter: () => void
|
||||
option: ComponentPickerOption
|
||||
}
|
||||
const ComponentPickerMenuItem: FC<ComponentPickerMenuItemProps> = ({
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
option,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
className={`
|
||||
flex items-center px-3 py-1.5 rounded-lg
|
||||
${isSelected && !option.disabled && '!bg-gray-50'}
|
||||
${option.disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={option.setRefElement}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}>
|
||||
<div className='flex items-center justify-center mr-2 w-8 h-8 rounded-lg border border-gray-100'>
|
||||
{option.icon}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='flex items-center justify-between h-5 text-sm text-gray-900'>
|
||||
{option.title}
|
||||
<span className='text-xs text-gray-400'>{option.disabled && t('common.promptEditor.existed')}</span>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{option.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ComponentPickerProps = {
|
||||
contextDisabled?: boolean
|
||||
historyDisabled?: boolean
|
||||
queryDisabled?: boolean
|
||||
contextShow?: boolean
|
||||
historyShow?: boolean
|
||||
queryShow?: boolean
|
||||
}
|
||||
const ComponentPicker: FC<ComponentPickerProps> = ({
|
||||
contextDisabled,
|
||||
historyDisabled,
|
||||
queryDisabled,
|
||||
contextShow,
|
||||
historyShow,
|
||||
queryShow,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
||||
minLength: 0,
|
||||
maxLength: 0,
|
||||
})
|
||||
|
||||
const options = [
|
||||
...contextShow
|
||||
? [
|
||||
new ComponentPickerOption(t('common.promptEditor.context.item.title'), {
|
||||
desc: t('common.promptEditor.context.item.desc'),
|
||||
icon: <File05 className='w-4 h-4 text-[#6938EF]' />,
|
||||
onSelect: () => {
|
||||
if (contextDisabled)
|
||||
return
|
||||
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
disabled: contextDisabled,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
new ComponentPickerOption(t('common.promptEditor.variable.item.title'), {
|
||||
desc: t('common.promptEditor.variable.item.desc'),
|
||||
icon: <Variable className='w-4 h-4 text-[#2970FF]' />,
|
||||
onSelect: () => {
|
||||
editor.dispatchCommand(INSERT_VARIABLE_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
}),
|
||||
...historyShow
|
||||
? [
|
||||
new ComponentPickerOption(t('common.promptEditor.history.item.title'), {
|
||||
desc: t('common.promptEditor.history.item.desc'),
|
||||
icon: <MessageClockCircle className='w-4 h-4 text-[#DD2590]' />,
|
||||
onSelect: () => {
|
||||
if (historyDisabled)
|
||||
return
|
||||
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
disabled: historyDisabled,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
...queryShow
|
||||
? [
|
||||
new ComponentPickerOption(t('common.promptEditor.query.item.title'), {
|
||||
desc: t('common.promptEditor.query.item.desc'),
|
||||
icon: <UserEdit02 className='w-4 h-4 text-[#FD853A]' />,
|
||||
onSelect: () => {
|
||||
if (queryDisabled)
|
||||
return
|
||||
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
disabled: queryDisabled,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
]
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(
|
||||
selectedOption: ComponentPickerOption,
|
||||
nodeToRemove: TextNode | null,
|
||||
closeMenu: () => void,
|
||||
matchingString: string,
|
||||
) => {
|
||||
editor.update(() => {
|
||||
if (nodeToRemove)
|
||||
nodeToRemove.remove()
|
||||
|
||||
selectedOption.onSelect(matchingString)
|
||||
closeMenu()
|
||||
})
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
options={options}
|
||||
onQueryChange={() => { }}
|
||||
onSelectOption={onSelectOption}
|
||||
anchorClassName='z-[999999]'
|
||||
menuRenderFn={(
|
||||
anchorElementRef,
|
||||
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
||||
) =>
|
||||
(anchorElementRef.current && options.length)
|
||||
? ReactDOM.createPortal(
|
||||
<div className='mt-[25px] p-1 w-[400px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
|
||||
{options.map((option, i: number) => (
|
||||
<ComponentPickerMenuItem
|
||||
isSelected={selectedIndex === i}
|
||||
onClick={() => {
|
||||
if (option.disabled)
|
||||
return
|
||||
setHighlightedIndex(i)
|
||||
selectOptionAndCleanUp(option)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (option.disabled)
|
||||
return
|
||||
setHighlightedIndex(i)
|
||||
}}
|
||||
key={option.key}
|
||||
option={option}
|
||||
/>
|
||||
))}
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
: null}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ComponentPicker
|
||||
@@ -18,12 +18,14 @@ type ContextBlockComponentProps = {
|
||||
nodeKey: string
|
||||
datasets?: Dataset[]
|
||||
onAddContext: () => void
|
||||
canNotAddContext?: boolean
|
||||
}
|
||||
|
||||
const ContextBlockComponent: FC<ContextBlockComponentProps> = ({
|
||||
nodeKey,
|
||||
datasets = [],
|
||||
onAddContext,
|
||||
canNotAddContext,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CONTEXT_BLOCK_COMMAND)
|
||||
@@ -44,52 +46,55 @@ const ContextBlockComponent: FC<ContextBlockComponentProps> = ({
|
||||
`} ref={ref}>
|
||||
<File05 className='mr-1 w-[14px] h-[14px]' />
|
||||
<div className='mr-1 text-xs font-medium'>{t('common.promptEditor.context.item.title')}</div>
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 3,
|
||||
alignmentAxis: -147,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger ref={triggerRef}>
|
||||
<div className={`
|
||||
{!canNotAddContext && (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 3,
|
||||
alignmentAxis: -147,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger ref={triggerRef}>
|
||||
<div className={`
|
||||
flex items-center justify-center w-[18px] h-[18px] text-[11px] font-semibold rounded cursor-pointer
|
||||
${open ? 'bg-[#6938EF] text-white' : 'bg-white/50 group-hover:bg-white group-hover:shadow-xs'}
|
||||
`}>{localDatasets.length}</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 100 }}>
|
||||
<div className='w-[360px] bg-white rounded-xl shadow-lg'>
|
||||
<div className='p-4'>
|
||||
<div className='mb-2 text-xs font-medium text-gray-500'>
|
||||
{t('common.promptEditor.context.modal.title', { num: localDatasets.length })}
|
||||
</div>
|
||||
<div className='max-h-[270px] overflow-y-auto'>
|
||||
{
|
||||
localDatasets.map(dataset => (
|
||||
<div key={dataset.id} className='flex items-center h-8'>
|
||||
<div className='flex items-center justify-center shrink-0 mr-2 w-6 h-6 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#EAECF5]'>
|
||||
<Folder className='w-4 h-4 text-[#444CE7]' />
|
||||
</div>
|
||||
<div className='text-sm text-gray-800 truncate' title=''>{dataset.name}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='flex items-center h-8 text-[#155EEF] cursor-pointer' onClick={onAddContext}>
|
||||
<div className='shrink-0 flex justify-center items-center mr-2 w-6 h-6 rounded-md border-[0.5px] border-gray-100'>
|
||||
<Plus className='w-[14px] h-[14px]' />
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 100 }}>
|
||||
<div className='w-[360px] bg-white rounded-xl shadow-lg'>
|
||||
<div className='p-4'>
|
||||
<div className='mb-2 text-xs font-medium text-gray-500'>
|
||||
{t('common.promptEditor.context.modal.title', { num: localDatasets.length })}
|
||||
</div>
|
||||
<div className='text-[13px] font-medium' title=''>{t('common.promptEditor.context.modal.add')}</div>
|
||||
<div className='max-h-[270px] overflow-y-auto'>
|
||||
{
|
||||
localDatasets.map(dataset => (
|
||||
<div key={dataset.id} className='flex items-center h-8'>
|
||||
<div className='flex items-center justify-center shrink-0 mr-2 w-6 h-6 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#EAECF5]'>
|
||||
<Folder className='w-4 h-4 text-[#444CE7]' />
|
||||
</div>
|
||||
<div className='text-sm text-gray-800 truncate' title=''>{dataset.name}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='flex items-center h-8 text-[#155EEF] cursor-pointer' onClick={onAddContext}>
|
||||
<div className='shrink-0 flex justify-center items-center mr-2 w-6 h-6 rounded-md border-[0.5px] border-gray-100'>
|
||||
<Plus className='w-[14px] h-[14px]' />
|
||||
</div>
|
||||
<div className='text-[13px] font-medium' title=''>{t('common.promptEditor.context.modal.add')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-4 py-3 text-xs text-gray-500 bg-gray-50 border-t-[0.5px] border-gray-50 rounded-b-xl'>
|
||||
{t('common.promptEditor.context.modal.footer')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-4 py-3 text-xs text-gray-500 bg-gray-50 border-t-[0.5px] border-gray-50 rounded-b-xl'>
|
||||
{t('common.promptEditor.context.modal.footer')}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../utils'
|
||||
import { CONTEXT_PLACEHOLDER_TEXT } from '../constants'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { CONTEXT_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { ContextBlockType } from '../../types'
|
||||
import {
|
||||
$createContextBlockNode,
|
||||
ContextBlockNode,
|
||||
} from './context-block/node'
|
||||
import type { ContextBlockProps } from './context-block/index'
|
||||
import { CustomTextNode } from './custom-text/node'
|
||||
} from '../context-block/node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT)
|
||||
|
||||
const ContextBlockReplacementBlock: FC<ContextBlockProps> = ({
|
||||
datasets,
|
||||
onAddContext,
|
||||
const ContextBlockReplacementBlock = ({
|
||||
datasets = [],
|
||||
onAddContext = () => {},
|
||||
onInsert,
|
||||
}) => {
|
||||
canNotAddContext,
|
||||
}: ContextBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,8 +33,8 @@ const ContextBlockReplacementBlock: FC<ContextBlockProps> = ({
|
||||
const createContextBlockNode = useCallback((): ContextBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext))
|
||||
}, [datasets, onAddContext, onInsert])
|
||||
return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext, canNotAddContext))
|
||||
}, [datasets, onAddContext, onInsert, canNotAddContext])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
@@ -50,6 +51,7 @@ const ContextBlockReplacementBlock: FC<ContextBlockProps> = ({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)),
|
||||
)
|
||||
@@ -58,4 +60,4 @@ const ContextBlockReplacementBlock: FC<ContextBlockProps> = ({
|
||||
return null
|
||||
}
|
||||
|
||||
export default ContextBlockReplacementBlock
|
||||
export default memo(ContextBlockReplacementBlock)
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
@@ -7,6 +9,7 @@ import {
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { ContextBlockType } from '../../types'
|
||||
import {
|
||||
$createContextBlockNode,
|
||||
ContextBlockNode,
|
||||
@@ -21,18 +24,13 @@ export type Dataset = {
|
||||
type: string
|
||||
}
|
||||
|
||||
export type ContextBlockProps = {
|
||||
datasets: Dataset[]
|
||||
onAddContext: () => void
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
const ContextBlock: FC<ContextBlockProps> = ({
|
||||
datasets,
|
||||
onAddContext,
|
||||
const ContextBlock = memo(({
|
||||
datasets = [],
|
||||
onAddContext = () => {},
|
||||
onInsert,
|
||||
onDelete,
|
||||
}) => {
|
||||
canNotAddContext,
|
||||
}: ContextBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,7 +41,7 @@ const ContextBlock: FC<ContextBlockProps> = ({
|
||||
editor.registerCommand(
|
||||
INSERT_CONTEXT_BLOCK_COMMAND,
|
||||
() => {
|
||||
const contextBlockNode = $createContextBlockNode(datasets, onAddContext)
|
||||
const contextBlockNode = $createContextBlockNode(datasets, onAddContext, canNotAddContext)
|
||||
|
||||
$insertNodes([contextBlockNode])
|
||||
|
||||
@@ -65,9 +63,12 @@ const ContextBlock: FC<ContextBlockProps> = ({
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, datasets, onAddContext, onInsert, onDelete])
|
||||
}, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext])
|
||||
|
||||
return null
|
||||
}
|
||||
})
|
||||
ContextBlock.displayName = 'ContextBlock'
|
||||
|
||||
export default ContextBlock
|
||||
export { ContextBlock }
|
||||
export { ContextBlockNode } from './node'
|
||||
export { default as ContextBlockReplacementBlock } from './context-block-replacement-block'
|
||||
|
||||
@@ -3,29 +3,31 @@ import { DecoratorNode } from 'lexical'
|
||||
import ContextBlockComponent from './component'
|
||||
import type { Dataset } from './index'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode & { datasets: Dataset[]; onAddContext: () => void }
|
||||
export type SerializedNode = SerializedLexicalNode & { datasets: Dataset[]; onAddContext: () => void; canNotAddContext: boolean }
|
||||
|
||||
export class ContextBlockNode extends DecoratorNode<JSX.Element> {
|
||||
__datasets: Dataset[]
|
||||
__onAddContext: () => void
|
||||
__canNotAddContext: boolean
|
||||
|
||||
static getType(): string {
|
||||
return 'context-block'
|
||||
}
|
||||
|
||||
static clone(node: ContextBlockNode): ContextBlockNode {
|
||||
return new ContextBlockNode(node.__datasets, node.__onAddContext)
|
||||
return new ContextBlockNode(node.__datasets, node.__onAddContext, node.getKey(), node.__canNotAddContext)
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(datasets: Dataset[], onAddContext: () => void, key?: NodeKey) {
|
||||
constructor(datasets: Dataset[], onAddContext: () => void, key?: NodeKey, canNotAddContext?: boolean) {
|
||||
super(key)
|
||||
|
||||
this.__datasets = datasets
|
||||
this.__onAddContext = onAddContext
|
||||
this.__canNotAddContext = canNotAddContext || false
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
@@ -44,6 +46,7 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
|
||||
nodeKey={this.getKey()}
|
||||
datasets={this.getDatasets()}
|
||||
onAddContext={this.getOnAddContext()}
|
||||
canNotAddContext={this.getCanNotAddContext()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -60,8 +63,14 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
|
||||
return self.__onAddContext
|
||||
}
|
||||
|
||||
getCanNotAddContext(): boolean {
|
||||
const self = this.getLatest()
|
||||
|
||||
return self.__canNotAddContext
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedNode): ContextBlockNode {
|
||||
const node = $createContextBlockNode(serializedNode.datasets, serializedNode.onAddContext)
|
||||
const node = $createContextBlockNode(serializedNode.datasets, serializedNode.onAddContext, serializedNode.canNotAddContext)
|
||||
|
||||
return node
|
||||
}
|
||||
@@ -72,6 +81,7 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
|
||||
version: 1,
|
||||
datasets: this.getDatasets(),
|
||||
onAddContext: this.getOnAddContext(),
|
||||
canNotAddContext: this.getCanNotAddContext(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +89,8 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
|
||||
return '{{#context#}}'
|
||||
}
|
||||
}
|
||||
export function $createContextBlockNode(datasets: Dataset[], onAddContext: () => void): ContextBlockNode {
|
||||
return new ContextBlockNode(datasets, onAddContext)
|
||||
export function $createContextBlockNode(datasets: Dataset[], onAddContext: () => void, canNotAddContext?: boolean): ContextBlockNode {
|
||||
return new ContextBlockNode(datasets, onAddContext, undefined, canNotAddContext)
|
||||
}
|
||||
|
||||
export function $isContextBlockNode(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -6,22 +5,22 @@ import {
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../utils'
|
||||
import { HISTORY_PLACEHOLDER_TEXT } from '../constants'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { HISTORY_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { HistoryBlockType } from '../../types'
|
||||
import {
|
||||
$createHistoryBlockNode,
|
||||
HistoryBlockNode,
|
||||
} from './history-block/node'
|
||||
import type { HistoryBlockProps } from './history-block/index'
|
||||
import { CustomTextNode } from './custom-text/node'
|
||||
} from '../history-block/node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
|
||||
|
||||
const HistoryBlockReplacementBlock: FC<HistoryBlockProps> = ({
|
||||
roleName,
|
||||
onEditRole,
|
||||
const HistoryBlockReplacementBlock = ({
|
||||
history = { user: '', assistant: '' },
|
||||
onEditRole = () => {},
|
||||
onInsert,
|
||||
}) => {
|
||||
}: HistoryBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,8 +31,8 @@ const HistoryBlockReplacementBlock: FC<HistoryBlockProps> = ({
|
||||
const createHistoryBlockNode = useCallback((): HistoryBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createHistoryBlockNode(roleName, onEditRole))
|
||||
}, [roleName, onEditRole, onInsert])
|
||||
return $applyNodeReplacement($createHistoryBlockNode(history, onEditRole))
|
||||
}, [history, onEditRole, onInsert])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
@@ -50,6 +49,7 @@ const HistoryBlockReplacementBlock: FC<HistoryBlockProps> = ({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)),
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
@@ -7,6 +9,7 @@ import {
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { HistoryBlockType } from '../../types'
|
||||
import {
|
||||
$createHistoryBlockNode,
|
||||
HistoryBlockNode,
|
||||
@@ -27,12 +30,12 @@ export type HistoryBlockProps = {
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
const HistoryBlock: FC<HistoryBlockProps> = ({
|
||||
roleName,
|
||||
onEditRole,
|
||||
const HistoryBlock = memo(({
|
||||
history = { user: '', assistant: '' },
|
||||
onEditRole = () => {},
|
||||
onInsert,
|
||||
onDelete,
|
||||
}) => {
|
||||
}: HistoryBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,7 +46,7 @@ const HistoryBlock: FC<HistoryBlockProps> = ({
|
||||
editor.registerCommand(
|
||||
INSERT_HISTORY_BLOCK_COMMAND,
|
||||
() => {
|
||||
const historyBlockNode = $createHistoryBlockNode(roleName, onEditRole)
|
||||
const historyBlockNode = $createHistoryBlockNode(history, onEditRole)
|
||||
|
||||
$insertNodes([historyBlockNode])
|
||||
|
||||
@@ -65,9 +68,12 @@ const HistoryBlock: FC<HistoryBlockProps> = ({
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, roleName, onEditRole, onInsert, onDelete])
|
||||
}, [editor, history, onEditRole, onInsert, onDelete])
|
||||
|
||||
return null
|
||||
}
|
||||
})
|
||||
HistoryBlock.displayName = 'HistoryBlock'
|
||||
|
||||
export default HistoryBlock
|
||||
export { HistoryBlock }
|
||||
export { HistoryBlockNode } from './node'
|
||||
export { default as HistoryBlockReplacementBlock } from './history-block-replacement-block'
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
BLUR_COMMAND,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
|
||||
type OnBlurBlockProps = {
|
||||
onBlur?: () => void
|
||||
}
|
||||
const OnBlurBlock: FC<OnBlurBlockProps> = ({
|
||||
onBlur,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
BLUR_COMMAND,
|
||||
() => {
|
||||
if (onBlur)
|
||||
onBlur()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onBlur])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default OnBlurBlock
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import {
|
||||
BLUR_COMMAND,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
FOCUS_COMMAND,
|
||||
KEY_ESCAPE_COMMAND,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
|
||||
|
||||
type OnBlurBlockProps = {
|
||||
onBlur?: () => void
|
||||
onFocus?: () => void
|
||||
}
|
||||
const OnBlurBlock: FC<OnBlurBlockProps> = ({
|
||||
onBlur,
|
||||
onFocus,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const ref = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
CLEAR_HIDE_MENU_TIMEOUT,
|
||||
() => {
|
||||
if (ref.current) {
|
||||
clearTimeout(ref.current)
|
||||
ref.current = null
|
||||
}
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
BLUR_COMMAND,
|
||||
() => {
|
||||
ref.current = setTimeout(() => {
|
||||
editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' }))
|
||||
}, 100)
|
||||
|
||||
if (onBlur)
|
||||
onBlur()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
FOCUS_COMMAND,
|
||||
() => {
|
||||
if (onFocus)
|
||||
onFocus()
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onBlur, onFocus])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default OnBlurBlock
|
||||
@@ -1,13 +1,27 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
|
||||
const Placeholder = () => {
|
||||
const Placeholder = ({
|
||||
compact,
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
compact?: boolean
|
||||
value?: string
|
||||
className?: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='absolute top-0 left-0 h-full w-full text-sm text-gray-300 select-none pointer-events-none leading-6'>
|
||||
{t('common.promptEditor.placeholder')}
|
||||
<div className={cn(
|
||||
className,
|
||||
'absolute top-0 left-0 h-full w-full text-sm text-gray-300 select-none pointer-events-none',
|
||||
compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm',
|
||||
)}>
|
||||
{value || t('common.promptEditor.placeholder')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Placeholder
|
||||
export default memo(Placeholder)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
@@ -7,6 +9,7 @@ import {
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { QueryBlockType } from '../../types'
|
||||
import {
|
||||
$createQueryBlockNode,
|
||||
QueryBlockNode,
|
||||
@@ -19,10 +22,10 @@ export type QueryBlockProps = {
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
const QueryBlock: FC<QueryBlockProps> = ({
|
||||
const QueryBlock = memo(({
|
||||
onInsert,
|
||||
onDelete,
|
||||
}) => {
|
||||
}: QueryBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,6 +60,9 @@ const QueryBlock: FC<QueryBlockProps> = ({
|
||||
}, [editor, onInsert, onDelete])
|
||||
|
||||
return null
|
||||
}
|
||||
})
|
||||
QueryBlock.displayName = 'QueryBlock'
|
||||
|
||||
export default QueryBlock
|
||||
export { QueryBlock }
|
||||
export { QueryBlockNode } from './node'
|
||||
export { default as QueryBlockReplacementBlock } from './query-block-replacement-block'
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../utils'
|
||||
import { QUERY_PLACEHOLDER_TEXT } from '../constants'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { QUERY_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { QueryBlockType } from '../../types'
|
||||
import {
|
||||
$createQueryBlockNode,
|
||||
QueryBlockNode,
|
||||
} from './query-block/node'
|
||||
import type { QueryBlockProps } from './query-block/index'
|
||||
import { CustomTextNode } from './custom-text/node'
|
||||
} from '../query-block/node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT)
|
||||
|
||||
const QueryBlockReplacementBlock: FC<QueryBlockProps> = ({
|
||||
const QueryBlockReplacementBlock = ({
|
||||
onInsert,
|
||||
}) => {
|
||||
}: QueryBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -48,6 +48,7 @@ const QueryBlockReplacementBlock: FC<QueryBlockProps> = ({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)),
|
||||
)
|
||||
@@ -56,4 +57,4 @@ const QueryBlockReplacementBlock: FC<QueryBlockProps> = ({
|
||||
return null
|
||||
}
|
||||
|
||||
export default QueryBlockReplacementBlock
|
||||
export default memo(QueryBlockReplacementBlock)
|
||||
@@ -1,20 +1,41 @@
|
||||
import { $insertNodes } from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { textToEditorState } from '../utils'
|
||||
import { CustomTextNode } from './custom-text/node'
|
||||
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER'
|
||||
export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY'
|
||||
|
||||
const UpdateBlock = () => {
|
||||
type UpdateBlockProps = {
|
||||
instanceId?: string
|
||||
}
|
||||
const UpdateBlock = ({
|
||||
instanceId,
|
||||
}: UpdateBlockProps) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER) {
|
||||
if (v.type === PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER && v.instanceId === instanceId) {
|
||||
const editorState = editor.parseEditorState(textToEditorState(v.payload))
|
||||
editor.setEditorState(editorState)
|
||||
}
|
||||
})
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === PROMPT_EDITOR_INSERT_QUICKLY && v.instanceId === instanceId) {
|
||||
editor.focus()
|
||||
editor.update(() => {
|
||||
const textNode = new CustomTextNode('/')
|
||||
$insertNodes([textNode])
|
||||
|
||||
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { $insertNodes, type TextNode } from 'lexical'
|
||||
import {
|
||||
LexicalTypeaheadMenuPlugin,
|
||||
MenuOption,
|
||||
} from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useBasicTypeaheadTriggerMatch } from '../hooks'
|
||||
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from './variable-block'
|
||||
import { $createCustomTextNode } from './custom-text/node'
|
||||
import { BracketsX } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import { Tool03 } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
class VariablePickerOption extends MenuOption {
|
||||
title: string
|
||||
icon?: JSX.Element
|
||||
extraElement?: JSX.Element
|
||||
keywords: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
|
||||
constructor(
|
||||
title: string,
|
||||
options: {
|
||||
icon?: JSX.Element
|
||||
extraElement?: JSX.Element
|
||||
keywords?: Array<string>
|
||||
keyboardShortcut?: string
|
||||
onSelect: (queryString: string) => void
|
||||
},
|
||||
) {
|
||||
super(title)
|
||||
this.title = title
|
||||
this.keywords = options.keywords || []
|
||||
this.icon = options.icon
|
||||
this.extraElement = options.extraElement
|
||||
this.keyboardShortcut = options.keyboardShortcut
|
||||
this.onSelect = options.onSelect.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
type VariablePickerMenuItemProps = {
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
onMouseEnter: () => void
|
||||
option: VariablePickerOption
|
||||
queryString: string | null
|
||||
}
|
||||
const VariablePickerMenuItem: FC<VariablePickerMenuItemProps> = ({
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
option,
|
||||
queryString,
|
||||
}) => {
|
||||
const title = option.title
|
||||
let before = title
|
||||
let middle = ''
|
||||
let after = ''
|
||||
|
||||
if (queryString) {
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
const match = regex.exec(option.title)
|
||||
|
||||
if (match) {
|
||||
before = title.substring(0, match.index)
|
||||
middle = match[0]
|
||||
after = title.substring(match.index + match[0].length)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
className={`
|
||||
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
|
||||
${isSelected && 'bg-primary-50'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={option.setRefElement}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}>
|
||||
<div className='mr-2'>
|
||||
{option.icon}
|
||||
</div>
|
||||
<div className='grow text-[13px] text-gray-900 truncate' title={option.title}>
|
||||
{before}
|
||||
<span className='text-[#2970FF]'>{middle}</span>
|
||||
{after}
|
||||
</div>
|
||||
{option.extraElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type Option = {
|
||||
value: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type ExternalToolOption = {
|
||||
name: string
|
||||
variableName: string
|
||||
icon?: string
|
||||
icon_background?: string
|
||||
}
|
||||
|
||||
type VariablePickerProps = {
|
||||
items?: Option[]
|
||||
externalTools?: ExternalToolOption[]
|
||||
onAddExternalTool?: () => void
|
||||
}
|
||||
const VariablePicker: FC<VariablePickerProps> = ({
|
||||
items = [],
|
||||
externalTools = [],
|
||||
onAddExternalTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('{', {
|
||||
minLength: 0,
|
||||
maxLength: 6,
|
||||
})
|
||||
const [queryString, setQueryString] = useState<string | null>(null)
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
|
||||
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
|
||||
})
|
||||
|
||||
const options = useMemo(() => {
|
||||
const baseOptions = items.map((item) => {
|
||||
return new VariablePickerOption(item.value, {
|
||||
icon: <BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />,
|
||||
onSelect: () => {
|
||||
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
|
||||
},
|
||||
})
|
||||
})
|
||||
if (!queryString)
|
||||
return baseOptions
|
||||
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
|
||||
return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
|
||||
}, [editor, queryString, items])
|
||||
|
||||
const toolOptions = useMemo(() => {
|
||||
const baseToolOptions = externalTools.map((item) => {
|
||||
return new VariablePickerOption(item.name, {
|
||||
icon: (
|
||||
<AppIcon
|
||||
className='!w-[14px] !h-[14px]'
|
||||
icon={item.icon}
|
||||
background={item.icon_background}
|
||||
/>
|
||||
),
|
||||
extraElement: <div className='text-xs text-gray-400'>{item.variableName}</div>,
|
||||
onSelect: () => {
|
||||
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
|
||||
},
|
||||
})
|
||||
})
|
||||
if (!queryString)
|
||||
return baseToolOptions
|
||||
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
|
||||
return baseToolOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
|
||||
}, [editor, queryString, externalTools])
|
||||
|
||||
const newOption = new VariablePickerOption(t('common.promptEditor.variable.modal.add'), {
|
||||
icon: <BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />,
|
||||
onSelect: () => {
|
||||
editor.update(() => {
|
||||
const prefixNode = $createCustomTextNode('{{')
|
||||
const suffixNode = $createCustomTextNode('}}')
|
||||
$insertNodes([prefixNode, suffixNode])
|
||||
prefixNode.select()
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const newToolOption = new VariablePickerOption(t('common.promptEditor.variable.modal.addTool'), {
|
||||
icon: <Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />,
|
||||
extraElement: <ArrowUpRight className='w-3 h-3 text-gray-400' />,
|
||||
onSelect: () => {
|
||||
if (onAddExternalTool)
|
||||
onAddExternalTool()
|
||||
},
|
||||
})
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(
|
||||
selectedOption: VariablePickerOption,
|
||||
nodeToRemove: TextNode | null,
|
||||
closeMenu: () => void,
|
||||
matchingString: string,
|
||||
) => {
|
||||
editor.update(() => {
|
||||
if (nodeToRemove)
|
||||
nodeToRemove.remove()
|
||||
|
||||
selectedOption.onSelect(matchingString)
|
||||
closeMenu()
|
||||
})
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
|
||||
const mergedOptions = [...options, ...toolOptions, newOption, newToolOption]
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
options={mergedOptions}
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
anchorClassName='z-[999999]'
|
||||
menuRenderFn={(
|
||||
anchorElementRef,
|
||||
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
||||
) =>
|
||||
(anchorElementRef.current && mergedOptions.length)
|
||||
? ReactDOM.createPortal(
|
||||
<div className='mt-[25px] w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
|
||||
{
|
||||
!!options.length && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
{options.map((option, i: number) => (
|
||||
<VariablePickerMenuItem
|
||||
isSelected={selectedIndex === i}
|
||||
onClick={() => {
|
||||
setHighlightedIndex(i)
|
||||
selectOptionAndCleanUp(option)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedIndex(i)
|
||||
}}
|
||||
key={option.key}
|
||||
option={option}
|
||||
queryString={queryString}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='h-[1px] bg-gray-100' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!toolOptions.length && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
{toolOptions.map((option, i: number) => (
|
||||
<VariablePickerMenuItem
|
||||
isSelected={selectedIndex === i + options.length}
|
||||
onClick={() => {
|
||||
setHighlightedIndex(i + options.length)
|
||||
selectOptionAndCleanUp(option)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedIndex(i + options.length)
|
||||
}}
|
||||
key={option.key}
|
||||
option={option}
|
||||
queryString={queryString}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='h-[1px] bg-gray-100' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
|
||||
${selectedIndex === options.length + toolOptions.length && 'bg-primary-50'}
|
||||
`}
|
||||
ref={newOption.setRefElement}
|
||||
tabIndex={-1}
|
||||
onClick={() => {
|
||||
setHighlightedIndex(options.length + toolOptions.length)
|
||||
selectOptionAndCleanUp(newOption)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedIndex(options.length + toolOptions.length)
|
||||
}}
|
||||
key={newOption.key}
|
||||
>
|
||||
{newOption.icon}
|
||||
<div className='text-[13px] text-gray-900'>{newOption.title}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
|
||||
${selectedIndex === options.length + toolOptions.length + 1 && 'bg-primary-50'}
|
||||
`}
|
||||
ref={newToolOption.setRefElement}
|
||||
tabIndex={-1}
|
||||
onClick={() => {
|
||||
setHighlightedIndex(options.length + toolOptions.length + 1)
|
||||
selectOptionAndCleanUp(newToolOption)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedIndex(options.length + toolOptions.length + 1)
|
||||
}}
|
||||
key={newToolOption.key}
|
||||
>
|
||||
{newToolOption.icon}
|
||||
<div className='grow text-[13px] text-gray-900'>{newToolOption.title}</div>
|
||||
{newToolOption.extraElement}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
: null}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariablePicker
|
||||
@@ -1,236 +1,5 @@
|
||||
function getHashtagRegexStringChars(): Readonly<{
|
||||
alpha: string
|
||||
alphanumeric: string
|
||||
leftChars: string
|
||||
rightChars: string
|
||||
}> {
|
||||
// Latin accented characters
|
||||
// Excludes 0xd7 from the range
|
||||
// (the multiplication sign, confusable with "x").
|
||||
// Also excludes 0xf7, the division sign
|
||||
const latinAccents
|
||||
= '\xC0-\xD6'
|
||||
+ '\xD8-\xF6'
|
||||
+ '\xF8-\xFF'
|
||||
+ '\u0100-\u024F'
|
||||
+ '\u0253-\u0254'
|
||||
+ '\u0256-\u0257'
|
||||
+ '\u0259'
|
||||
+ '\u025B'
|
||||
+ '\u0263'
|
||||
+ '\u0268'
|
||||
+ '\u026F'
|
||||
+ '\u0272'
|
||||
+ '\u0289'
|
||||
+ '\u028B'
|
||||
+ '\u02BB'
|
||||
+ '\u0300-\u036F'
|
||||
+ '\u1E00-\u1EFF'
|
||||
|
||||
// Cyrillic (Russian, Ukrainian, etc.)
|
||||
const nonLatinChars
|
||||
= '\u0400-\u04FF' // Cyrillic
|
||||
+ '\u0500-\u0527' // Cyrillic Supplement
|
||||
+ '\u2DE0-\u2DFF' // Cyrillic Extended A
|
||||
+ '\uA640-\uA69F' // Cyrillic Extended B
|
||||
+ '\u0591-\u05BF' // Hebrew
|
||||
+ '\u05C1-\u05C2'
|
||||
+ '\u05C4-\u05C5'
|
||||
+ '\u05C7'
|
||||
+ '\u05D0-\u05EA'
|
||||
+ '\u05F0-\u05F4'
|
||||
+ '\uFB12-\uFB28' // Hebrew Presentation Forms
|
||||
+ '\uFB2A-\uFB36'
|
||||
+ '\uFB38-\uFB3C'
|
||||
+ '\uFB3E'
|
||||
+ '\uFB40-\uFB41'
|
||||
+ '\uFB43-\uFB44'
|
||||
+ '\uFB46-\uFB4F'
|
||||
+ '\u0610-\u061A' // Arabic
|
||||
+ '\u0620-\u065F'
|
||||
+ '\u066E-\u06D3'
|
||||
+ '\u06D5-\u06DC'
|
||||
+ '\u06DE-\u06E8'
|
||||
+ '\u06EA-\u06EF'
|
||||
+ '\u06FA-\u06FC'
|
||||
+ '\u06FF'
|
||||
+ '\u0750-\u077F' // Arabic Supplement
|
||||
+ '\u08A0' // Arabic Extended A
|
||||
+ '\u08A2-\u08AC'
|
||||
+ '\u08E4-\u08FE'
|
||||
+ '\uFB50-\uFBB1' // Arabic Pres. Forms A
|
||||
+ '\uFBD3-\uFD3D'
|
||||
+ '\uFD50-\uFD8F'
|
||||
+ '\uFD92-\uFDC7'
|
||||
+ '\uFDF0-\uFDFB'
|
||||
+ '\uFE70-\uFE74' // Arabic Pres. Forms B
|
||||
+ '\uFE76-\uFEFC'
|
||||
+ '\u200C-\u200C' // Zero-Width Non-Joiner
|
||||
+ '\u0E01-\u0E3A' // Thai
|
||||
+ '\u0E40-\u0E4E' // Hangul (Korean)
|
||||
+ '\u1100-\u11FF' // Hangul Jamo
|
||||
+ '\u3130-\u3185' // Hangul Compatibility Jamo
|
||||
+ '\uA960-\uA97F' // Hangul Jamo Extended-A
|
||||
+ '\uAC00-\uD7AF' // Hangul Syllables
|
||||
+ '\uD7B0-\uD7FF' // Hangul Jamo Extended-B
|
||||
+ '\uFFA1-\uFFDC' // Half-width Hangul
|
||||
|
||||
const charCode = String.fromCharCode
|
||||
|
||||
const cjkChars
|
||||
= '\u30A1-\u30FA\u30FC-\u30FE' // Katakana (full-width)
|
||||
+ '\uFF66-\uFF9F' // Katakana (half-width)
|
||||
+ '\uFF10-\uFF19\uFF21-\uFF3A'
|
||||
+ '\uFF41-\uFF5A' // Latin (full-width)
|
||||
+ '\u3041-\u3096\u3099-\u309E' // Hiragana
|
||||
+ '\u3400-\u4DBF' // Kanji (CJK Extension A)
|
||||
+ `\u4E00-\u9FFF${// Kanji (Unified)
|
||||
// Disabled as it breaks the Regex.
|
||||
// charCode(0x20000) + '-' + charCode(0x2A6DF) + // Kanji (CJK Extension B)
|
||||
charCode(0x2A700)
|
||||
}-${
|
||||
charCode(0x2B73F) // Kanji (CJK Extension C)
|
||||
}${charCode(0x2B740)
|
||||
}-${
|
||||
charCode(0x2B81F) // Kanji (CJK Extension D)
|
||||
}${charCode(0x2F800)
|
||||
}-${
|
||||
charCode(0x2FA1F)
|
||||
}\u3003\u3005\u303B` // Kanji (CJK supplement)
|
||||
|
||||
const otherChars = latinAccents + nonLatinChars + cjkChars
|
||||
// equivalent of \p{L}
|
||||
|
||||
const unicodeLetters
|
||||
= '\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6'
|
||||
+ '\u00F8-\u0241\u0250-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EE\u037A\u0386'
|
||||
+ '\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03F5\u03F7-\u0481'
|
||||
+ '\u048A-\u04CE\u04D0-\u04F9\u0500-\u050F\u0531-\u0556\u0559\u0561-\u0587'
|
||||
+ '\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0640-\u064A\u066E-\u066F'
|
||||
+ '\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710'
|
||||
+ '\u0712-\u072F\u074D-\u076D\u0780-\u07A5\u07B1\u0904-\u0939\u093D\u0950'
|
||||
+ '\u0958-\u0961\u097D\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0'
|
||||
+ '\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1'
|
||||
+ '\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33'
|
||||
+ '\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D'
|
||||
+ '\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD'
|
||||
+ '\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30'
|
||||
+ '\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83'
|
||||
+ '\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F'
|
||||
+ '\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10'
|
||||
+ '\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C'
|
||||
+ '\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE'
|
||||
+ '\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39'
|
||||
+ '\u0D60-\u0D61\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6'
|
||||
+ '\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88'
|
||||
+ '\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7'
|
||||
+ '\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6'
|
||||
+ '\u0EDC-\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6A\u0F88-\u0F8B\u1000-\u1021'
|
||||
+ '\u1023-\u1027\u1029-\u102A\u1050-\u1055\u10A0-\u10C5\u10D0-\u10FA\u10FC'
|
||||
+ '\u1100-\u1159\u115F-\u11A2\u11A8-\u11F9\u1200-\u1248\u124A-\u124D'
|
||||
+ '\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0'
|
||||
+ '\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310'
|
||||
+ '\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C'
|
||||
+ '\u166F-\u1676\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711'
|
||||
+ '\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7'
|
||||
+ '\u17DC\u1820-\u1877\u1880-\u18A8\u1900-\u191C\u1950-\u196D\u1970-\u1974'
|
||||
+ '\u1980-\u19A9\u19C1-\u19C7\u1A00-\u1A16\u1D00-\u1DBF\u1E00-\u1E9B'
|
||||
+ '\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D'
|
||||
+ '\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC'
|
||||
+ '\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC'
|
||||
+ '\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107'
|
||||
+ '\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D'
|
||||
+ '\u212F-\u2131\u2133-\u2139\u213C-\u213F\u2145-\u2149\u2C00-\u2C2E'
|
||||
+ '\u2C30-\u2C5E\u2C80-\u2CE4\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96'
|
||||
+ '\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6'
|
||||
+ '\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3006\u3031-\u3035'
|
||||
+ '\u303B-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF'
|
||||
+ '\u3105-\u312C\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5'
|
||||
+ '\u4E00-\u9FBB\uA000-\uA48C\uA800-\uA801\uA803-\uA805\uA807-\uA80A'
|
||||
+ '\uA80C-\uA822\uAC00-\uD7A3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9'
|
||||
+ '\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C'
|
||||
+ '\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F'
|
||||
+ '\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A'
|
||||
+ '\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7'
|
||||
+ '\uFFDA-\uFFDC'
|
||||
|
||||
// equivalent of \p{Mn}\p{Mc}
|
||||
const unicodeAccents
|
||||
= '\u0300-\u036F\u0483-\u0486\u0591-\u05B9\u05BB-\u05BD\u05BF'
|
||||
+ '\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u0615\u064B-\u065E\u0670'
|
||||
+ '\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A'
|
||||
+ '\u07A6-\u07B0\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u0962-\u0963'
|
||||
+ '\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7'
|
||||
+ '\u09E2-\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D'
|
||||
+ '\u0A70-\u0A71\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD'
|
||||
+ '\u0AE2-\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D'
|
||||
+ '\u0B56-\u0B57\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7'
|
||||
+ '\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56'
|
||||
+ '\u0C82-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6'
|
||||
+ '\u0D02-\u0D03\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D82-\u0D83'
|
||||
+ '\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF3\u0E31\u0E34-\u0E3A'
|
||||
+ '\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19'
|
||||
+ '\u0F35\u0F37\u0F39\u0F3E-\u0F3F\u0F71-\u0F84\u0F86-\u0F87\u0F90-\u0F97'
|
||||
+ '\u0F99-\u0FBC\u0FC6\u102C-\u1032\u1036-\u1039\u1056-\u1059\u135F'
|
||||
+ '\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B6-\u17D3\u17DD'
|
||||
+ '\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8-\u19C9'
|
||||
+ '\u1A17-\u1A1B\u1DC0-\u1DC3\u20D0-\u20DC\u20E1\u20E5-\u20EB\u302A-\u302F'
|
||||
+ '\u3099-\u309A\uA802\uA806\uA80B\uA823-\uA827\uFB1E\uFE00-\uFE0F'
|
||||
+ '\uFE20-\uFE23'
|
||||
|
||||
// equivalent of \p{Dn}
|
||||
const unicodeDigits
|
||||
= '\u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F\u09E6-\u09EF'
|
||||
+ '\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F'
|
||||
+ '\u0CE6-\u0CEF\u0D66-\u0D6F\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29'
|
||||
+ '\u1040-\u1049\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9'
|
||||
+ '\uFF10-\uFF19'
|
||||
|
||||
// An alpha char is a unicode chars including unicode marks or
|
||||
// letter or char in otherChars range
|
||||
const alpha = unicodeLetters
|
||||
|
||||
// A numeric character is any with the number digit property, or
|
||||
// underscore. These characters can be included in hashtags, but a hashtag
|
||||
// cannot have only these characters.
|
||||
const numeric = `${unicodeDigits}_`
|
||||
|
||||
// Alphanumeric char is any alpha char or a unicode char with decimal
|
||||
// number property \p{Nd}
|
||||
const alphanumeric = alpha + numeric
|
||||
const leftChars = '{'
|
||||
const rightChars = '}'
|
||||
|
||||
return {
|
||||
alpha,
|
||||
alphanumeric,
|
||||
leftChars,
|
||||
rightChars,
|
||||
}
|
||||
}
|
||||
|
||||
export function getHashtagRegexString(): string {
|
||||
const { alpha, alphanumeric, leftChars, rightChars } = getHashtagRegexStringChars()
|
||||
|
||||
const hashtagAlpha = `[${alpha}]`
|
||||
const hashtagAlphanumeric = `[${alphanumeric}]`
|
||||
const hashLeftCharList = `[${leftChars}]`
|
||||
const hashRightCharList = `[${rightChars}]`
|
||||
|
||||
// A hashtag contains characters, numbers and underscores,
|
||||
// but not all numbers.
|
||||
const hashtag
|
||||
= `(${
|
||||
hashLeftCharList
|
||||
})`
|
||||
+ `(${
|
||||
hashLeftCharList
|
||||
})([a-zA-Z_][a-zA-Z0-9_]{0,29}`
|
||||
+ `)(${
|
||||
hashRightCharList
|
||||
})(${
|
||||
hashRightCharList
|
||||
})`
|
||||
const hashtag = '(\{)(\{)([a-zA-Z_][a-zA-Z0-9_]{0,29})(\})(\})'
|
||||
|
||||
return hashtag
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import type { WorkflowNodesMap } from './node'
|
||||
import { WorkflowVariableBlockNode } from './node'
|
||||
import {
|
||||
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||
UPDATE_WORKFLOW_NODES_MAP,
|
||||
} from './index'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
type WorkflowVariableBlockComponentProps = {
|
||||
nodeKey: string
|
||||
variables: string[]
|
||||
workflowNodesMap: WorkflowNodesMap
|
||||
}
|
||||
|
||||
const WorkflowVariableBlockComponent = ({
|
||||
nodeKey,
|
||||
variables,
|
||||
workflowNodesMap = {},
|
||||
}: WorkflowVariableBlockComponentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND)
|
||||
const variablesLength = variables.length
|
||||
const lastVariable = isSystemVar(variables) ? variables.join('.') : variables[variablesLength - 1]
|
||||
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
|
||||
const node = localWorkflowNodesMap![variables[0]]
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
UPDATE_WORKFLOW_NODES_MAP,
|
||||
(workflowNodesMap: WorkflowNodesMap) => {
|
||||
setLocalWorkflowNodesMap(workflowNodesMap)
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
const Item = (
|
||||
<div
|
||||
className={`
|
||||
mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border
|
||||
${isSelected ? ' border-[#84ADFF] bg-[#F5F8FF]' : ' border-black/5 bg-white'}
|
||||
${!node && '!border-[#F04438] !bg-[#FEF3F2]'}
|
||||
`}
|
||||
ref={ref}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
node?.type && (
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-500'
|
||||
type={node?.type}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='shrink-0 mx-0.5 text-xs font-medium text-gray-500 truncate' title={node?.title} style={{
|
||||
}}>{node?.title}</div>
|
||||
<Line3 className='mr-0.5 text-gray-300'></Line3>
|
||||
</div>
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='shrink-0 ml-0.5 text-xs font-medium truncate' title={lastVariable}>{lastVariable}</div>
|
||||
{
|
||||
!node && (
|
||||
<AlertCircle className='ml-0.5 w-3 h-3 text-[#D92D20]' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!node) {
|
||||
return (
|
||||
<TooltipPlus popupContent={t('workflow.errorMsg.invalidVariable')}>
|
||||
{Item}
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
|
||||
return Item
|
||||
}
|
||||
|
||||
export default memo(WorkflowVariableBlockComponent)
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type { WorkflowVariableBlockType } from '../../types'
|
||||
import {
|
||||
$createWorkflowVariableBlockNode,
|
||||
WorkflowVariableBlockNode,
|
||||
} from './node'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
|
||||
export const INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND')
|
||||
export const DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND')
|
||||
export const CLEAR_HIDE_MENU_TIMEOUT = createCommand('CLEAR_HIDE_MENU_TIMEOUT')
|
||||
export const UPDATE_WORKFLOW_NODES_MAP = createCommand('UPDATE_WORKFLOW_NODES_MAP')
|
||||
|
||||
export type WorkflowVariableBlockProps = {
|
||||
getWorkflowNode: (nodeId: string) => Node
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
const WorkflowVariableBlock = memo(({
|
||||
workflowNodesMap,
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: WorkflowVariableBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
editor.update(() => {
|
||||
editor.dispatchCommand(UPDATE_WORKFLOW_NODES_MAP, workflowNodesMap)
|
||||
})
|
||||
}, [editor, workflowNodesMap])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||
(variables: string[]) => {
|
||||
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
||||
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap)
|
||||
|
||||
$insertNodes([workflowVariableBlockNode])
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onInsert, onDelete, workflowNodesMap])
|
||||
|
||||
return null
|
||||
})
|
||||
WorkflowVariableBlock.displayName = 'WorkflowVariableBlock'
|
||||
|
||||
export { WorkflowVariableBlock }
|
||||
export { WorkflowVariableBlockNode } from './node'
|
||||
export { default as WorkflowVariableBlockReplacementBlock } from './workflow-variable-block-replacement-block'
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import type { WorkflowVariableBlockType } from '../../types'
|
||||
import WorkflowVariableBlockComponent from './component'
|
||||
|
||||
export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap']
|
||||
export type SerializedNode = SerializedLexicalNode & {
|
||||
variables: string[]
|
||||
workflowNodesMap: WorkflowNodesMap
|
||||
}
|
||||
|
||||
export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
|
||||
__variables: string[]
|
||||
__workflowNodesMap: WorkflowNodesMap
|
||||
|
||||
static getType(): string {
|
||||
return 'workflow-variable-block'
|
||||
}
|
||||
|
||||
static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
|
||||
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap)
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, key?: NodeKey) {
|
||||
super(key)
|
||||
|
||||
this.__variables = variables
|
||||
this.__workflowNodesMap = workflowNodesMap
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): JSX.Element {
|
||||
return (
|
||||
<WorkflowVariableBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
variables={this.__variables}
|
||||
workflowNodesMap={this.__workflowNodesMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
|
||||
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'workflow-variable-block',
|
||||
version: 1,
|
||||
variables: this.getVariables(),
|
||||
workflowNodesMap: this.getWorkflowNodesMap(),
|
||||
}
|
||||
}
|
||||
|
||||
getVariables(): string[] {
|
||||
const self = this.getLatest()
|
||||
return self.__variables
|
||||
}
|
||||
|
||||
getWorkflowNodesMap(): WorkflowNodesMap {
|
||||
const self = this.getLatest()
|
||||
return self.__workflowNodesMap
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return `{{#${this.getVariables().join('.')}#}}`
|
||||
}
|
||||
}
|
||||
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap): WorkflowVariableBlockNode {
|
||||
return new WorkflowVariableBlockNode(variables, workflowNodesMap)
|
||||
}
|
||||
|
||||
export function $isWorkflowVariableBlockNode(
|
||||
node: WorkflowVariableBlockNode | LexicalNode | null | undefined,
|
||||
): node is WorkflowVariableBlockNode {
|
||||
return node instanceof WorkflowVariableBlockNode
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import type { TextNode } from 'lexical'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import type { WorkflowVariableBlockType } from '../../types'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
import { $createWorkflowVariableBlockNode } from './node'
|
||||
import { WorkflowVariableBlockNode } from './index'
|
||||
import { VAR_REGEX as REGEX } from '@/config'
|
||||
|
||||
const WorkflowVariableBlockReplacementBlock = ({
|
||||
workflowNodesMap,
|
||||
onInsert,
|
||||
}: WorkflowVariableBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createWorkflowVariableBlockNode = useCallback((textNode: TextNode): WorkflowVariableBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
const nodePathString = textNode.getTextContent().slice(3, -3)
|
||||
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap))
|
||||
}, [onInsert, workflowNodesMap])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + matchArr[0].length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
const transformListener = useCallback((textNode: any) => {
|
||||
return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode)
|
||||
}, [createWorkflowVariableBlockNode, getMatch])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, transformListener),
|
||||
)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(WorkflowVariableBlockReplacementBlock)
|
||||
69
web/app/components/base/prompt-editor/types.ts
Normal file
69
web/app/components/base/prompt-editor/types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { Dataset } from './plugins/context-block'
|
||||
import type { RoleName } from './plugins/history-block'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
export type Option = {
|
||||
value: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type ExternalToolOption = {
|
||||
name: string
|
||||
variableName: string
|
||||
icon?: string
|
||||
icon_background?: string
|
||||
}
|
||||
|
||||
export type ContextBlockType = {
|
||||
show?: boolean
|
||||
selectable?: boolean
|
||||
datasets?: Dataset[]
|
||||
canNotAddContext?: boolean
|
||||
onAddContext?: () => void
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
export type QueryBlockType = {
|
||||
show?: boolean
|
||||
selectable?: boolean
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
export type HistoryBlockType = {
|
||||
show?: boolean
|
||||
selectable?: boolean
|
||||
history?: RoleName
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
onEditRole?: () => void
|
||||
}
|
||||
|
||||
export type VariableBlockType = {
|
||||
show?: boolean
|
||||
variables?: Option[]
|
||||
}
|
||||
|
||||
export type ExternalToolBlockType = {
|
||||
show?: boolean
|
||||
externalTools?: ExternalToolOption[]
|
||||
onAddExternalTool?: () => void
|
||||
}
|
||||
|
||||
export type WorkflowVariableBlockType = {
|
||||
show?: boolean
|
||||
variables?: NodeOutPutVar[]
|
||||
workflowNodesMap?: Record<string, Pick<Node['data'], 'title' | 'type'>>
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
export type MenuTextMatch = {
|
||||
leadOffset: number
|
||||
matchingString: string
|
||||
replaceableString: string
|
||||
}
|
||||
@@ -9,10 +9,13 @@ import type {
|
||||
} from 'lexical'
|
||||
import {
|
||||
$createTextNode,
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
} from 'lexical'
|
||||
import type { EntityMatch } from '@lexical/text'
|
||||
import { CustomTextNode } from './plugins/custom-text/node'
|
||||
import type { MenuTextMatch } from './types'
|
||||
|
||||
export function getSelectedNode(
|
||||
selection: RangeSelection,
|
||||
@@ -190,7 +193,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
|
||||
export const decoratorTransform = (
|
||||
node: CustomTextNode,
|
||||
getMatch: (text: string) => null | EntityMatch,
|
||||
createNode: () => LexicalNode,
|
||||
createNode: (textNode: TextNode) => LexicalNode,
|
||||
) => {
|
||||
if (!node.isSimpleText())
|
||||
return
|
||||
@@ -241,7 +244,7 @@ export const decoratorTransform = (
|
||||
else
|
||||
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
|
||||
|
||||
const replacementNode = createNode()
|
||||
const replacementNode = createNode(nodeToReplace)
|
||||
nodeToReplace.replace(replacementNode)
|
||||
|
||||
if (currentNode == null)
|
||||
@@ -249,6 +252,49 @@ export const decoratorTransform = (
|
||||
}
|
||||
}
|
||||
|
||||
function getFullMatchOffset(
|
||||
documentText: string,
|
||||
entryText: string,
|
||||
offset: number,
|
||||
): number {
|
||||
let triggerOffset = offset
|
||||
for (let i = triggerOffset; i <= entryText.length; i++) {
|
||||
if (documentText.substr(-i) === entryText.substr(0, i))
|
||||
triggerOffset = i
|
||||
}
|
||||
return triggerOffset
|
||||
}
|
||||
|
||||
export function $splitNodeContainingQuery(match: MenuTextMatch): TextNode | null {
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
||||
return null
|
||||
const anchor = selection.anchor
|
||||
if (anchor.type !== 'text')
|
||||
return null
|
||||
const anchorNode = anchor.getNode()
|
||||
if (!anchorNode.isSimpleText())
|
||||
return null
|
||||
const selectionOffset = anchor.offset
|
||||
const textContent = anchorNode.getTextContent().slice(0, selectionOffset)
|
||||
const characterOffset = match.replaceableString.length
|
||||
const queryOffset = getFullMatchOffset(
|
||||
textContent,
|
||||
match.matchingString,
|
||||
characterOffset,
|
||||
)
|
||||
const startOffset = selectionOffset - queryOffset
|
||||
if (startOffset < 0)
|
||||
return null
|
||||
let newNode
|
||||
if (startOffset === 0)
|
||||
[newNode] = anchorNode.splitText(selectionOffset)
|
||||
else
|
||||
[, newNode] = anchorNode.splitText(startOffset, selectionOffset)
|
||||
|
||||
return newNode
|
||||
}
|
||||
|
||||
export function textToEditorState(text: string) {
|
||||
const paragraph = text.split('\n')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user