dify
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { $insertNodes } from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import type {
|
||||
ContextBlockType,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
ExternalToolBlockType,
|
||||
HistoryBlockType,
|
||||
LastRunBlockType,
|
||||
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 { PromptMenuItem } from './prompt-option'
|
||||
import { VariableMenuItem } from './variable-option'
|
||||
import { PickerBlockMenuOption } from './menu'
|
||||
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'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export const usePromptOptions = (
|
||||
contextBlock?: ContextBlockType,
|
||||
queryBlock?: QueryBlockType,
|
||||
historyBlock?: HistoryBlockType,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const promptOptions: PickerBlockMenuOption[] = []
|
||||
if (contextBlock?.show) {
|
||||
promptOptions.push(new PickerBlockMenuOption({
|
||||
key: t('common.promptEditor.context.item.title'),
|
||||
group: 'prompt context',
|
||||
render: ({ isSelected, onSelect, onSetHighlight }) => {
|
||||
return <PromptMenuItem
|
||||
title={t('common.promptEditor.context.item.title')}
|
||||
icon={<File05 className='h-4 w-4 text-[#6938EF]' />}
|
||||
disabled={!contextBlock.selectable}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
onMouseEnter={onSetHighlight}
|
||||
/>
|
||||
},
|
||||
onSelect: () => {
|
||||
if (!contextBlock?.selectable)
|
||||
return
|
||||
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
if (queryBlock?.show) {
|
||||
promptOptions.push(
|
||||
new PickerBlockMenuOption({
|
||||
key: t('common.promptEditor.query.item.title'),
|
||||
group: 'prompt query',
|
||||
render: ({ isSelected, onSelect, onSetHighlight }) => {
|
||||
return (
|
||||
<PromptMenuItem
|
||||
title={t('common.promptEditor.query.item.title')}
|
||||
icon={<UserEdit02 className='h-4 w-4 text-[#FD853A]' />}
|
||||
disabled={!queryBlock.selectable}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
onMouseEnter={onSetHighlight}
|
||||
/>
|
||||
)
|
||||
},
|
||||
onSelect: () => {
|
||||
if (!queryBlock?.selectable)
|
||||
return
|
||||
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (historyBlock?.show) {
|
||||
promptOptions.push(
|
||||
new PickerBlockMenuOption({
|
||||
key: t('common.promptEditor.history.item.title'),
|
||||
group: 'prompt history',
|
||||
render: ({ isSelected, onSelect, onSetHighlight }) => {
|
||||
return (
|
||||
<PromptMenuItem
|
||||
title={t('common.promptEditor.history.item.title')}
|
||||
icon={<MessageClockCircle className='h-4 w-4 text-[#DD2590]' />}
|
||||
disabled={!historyBlock.selectable
|
||||
}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
onMouseEnter={onSetHighlight}
|
||||
/>
|
||||
)
|
||||
},
|
||||
onSelect: () => {
|
||||
if (!historyBlock?.selectable)
|
||||
return
|
||||
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
return promptOptions
|
||||
}
|
||||
|
||||
export const useVariableOptions = (
|
||||
variableBlock?: VariableBlockType,
|
||||
queryString?: string,
|
||||
): PickerBlockMenuOption[] => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (!variableBlock?.variables)
|
||||
return []
|
||||
|
||||
const baseOptions = (variableBlock.variables).map((item) => {
|
||||
return new PickerBlockMenuOption({
|
||||
key: item.value,
|
||||
group: 'prompt variable',
|
||||
render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
|
||||
return (
|
||||
<VariableMenuItem
|
||||
title={item.value}
|
||||
icon={<BracketsX className='h-[14px] w-[14px] text-text-accent' />}
|
||||
queryString={queryString}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
onMouseEnter={onSetHighlight}
|
||||
/>
|
||||
)
|
||||
},
|
||||
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.key))
|
||||
}, [editor, queryString, variableBlock])
|
||||
|
||||
const addOption = useMemo(() => {
|
||||
return new PickerBlockMenuOption({
|
||||
key: t('common.promptEditor.variable.modal.add'),
|
||||
group: 'prompt variable',
|
||||
render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
|
||||
return (
|
||||
<VariableMenuItem
|
||||
title={t('common.promptEditor.variable.modal.add')}
|
||||
icon={<BracketsX className='h-[14px] w-[14px] text-text-accent' />}
|
||||
queryString={queryString}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
onMouseEnter={onSetHighlight}
|
||||
/>
|
||||
)
|
||||
},
|
||||
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(() => {
|
||||
if (!externalToolBlockType?.externalTools)
|
||||
return []
|
||||
const baseToolOptions = (externalToolBlockType.externalTools).map((item) => {
|
||||
return new PickerBlockMenuOption({
|
||||
key: item.name,
|
||||
group: 'external tool',
|
||||
render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
|
||||
return (
|
||||
<VariableMenuItem
|
||||
title={item.name}
|
||||
icon={
|
||||
<AppIcon
|
||||
className='!h-[14px] !w-[14px]'
|
||||
icon={item.icon}
|
||||
background={item.icon_background}
|
||||
/>
|
||||
}
|
||||
extraElement={<div className='text-xs text-text-tertiary'>{item.variableName}</div>}
|
||||
queryString={queryString}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
onMouseEnter={onSetHighlight}
|
||||
/>
|
||||
)
|
||||
},
|
||||
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.key))
|
||||
}, [editor, queryString, externalToolBlockType])
|
||||
|
||||
const addOption = useMemo(() => {
|
||||
return new PickerBlockMenuOption({
|
||||
key: t('common.promptEditor.variable.modal.addTool'),
|
||||
group: 'external tool',
|
||||
render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
|
||||
return (
|
||||
<VariableMenuItem
|
||||
title={t('common.promptEditor.variable.modal.addTool')}
|
||||
icon={<Tool03 className='h-[14px] w-[14px] text-text-accent' />}
|
||||
extraElement={< ArrowUpRight className='h-3 w-3 text-text-tertiary' />}
|
||||
queryString={queryString}
|
||||
isSelected={isSelected}
|
||||
onClick={onSelect}
|
||||
onMouseEnter={onSetHighlight}
|
||||
/>
|
||||
)
|
||||
},
|
||||
onSelect: () => {
|
||||
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,
|
||||
currentBlockType?: CurrentBlockType,
|
||||
errorMessageBlockType?: ErrorMessageBlockType,
|
||||
lastRunBlockType?: LastRunBlockType,
|
||||
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 []
|
||||
const res = workflowVariableBlockType.variables || []
|
||||
if(errorMessageBlockType?.show && res.findIndex(v => v.nodeId === 'error_message') === -1) {
|
||||
res.unshift({
|
||||
nodeId: 'error_message',
|
||||
title: 'error_message',
|
||||
isFlat: true,
|
||||
vars: [
|
||||
{
|
||||
variable: 'error_message',
|
||||
type: VarType.string,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
if(lastRunBlockType?.show && res.findIndex(v => v.nodeId === 'last_run') === -1) {
|
||||
res.unshift({
|
||||
nodeId: 'last_run',
|
||||
title: 'last_run',
|
||||
isFlat: true,
|
||||
vars: [
|
||||
{
|
||||
variable: 'last_run',
|
||||
type: VarType.object,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
if(currentBlockType?.show && res.findIndex(v => v.nodeId === 'current') === -1) {
|
||||
const title = currentBlockType.generatorType === 'prompt' ? 'current_prompt' : 'current_code'
|
||||
res.unshift({
|
||||
nodeId: 'current',
|
||||
title,
|
||||
isFlat: true,
|
||||
vars: [
|
||||
{
|
||||
variable: 'current',
|
||||
type: VarType.string,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
return res
|
||||
}, [workflowVariableBlockType?.show, workflowVariableBlockType?.variables, errorMessageBlockType?.show, lastRunBlockType?.show, currentBlockType?.show, currentBlockType?.generatorType])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
workflowVariableOptions,
|
||||
allFlattenOptions: [...promptOptions, ...variableOptions, ...externalToolOptions],
|
||||
}
|
||||
}, [promptOptions, variableOptions, externalToolOptions, workflowVariableOptions])
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {
|
||||
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,
|
||||
CurrentBlockType,
|
||||
ErrorMessageBlockType,
|
||||
ExternalToolBlockType,
|
||||
HistoryBlockType,
|
||||
LastRunBlockType,
|
||||
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 { useOptions } from './hooks'
|
||||
import type { PickerBlockMenuOption } from './menu'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { KEY_ESCAPE_COMMAND } from 'lexical'
|
||||
import { INSERT_CURRENT_BLOCK_COMMAND } from '../current-block'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import { INSERT_ERROR_MESSAGE_BLOCK_COMMAND } from '../error-message-block'
|
||||
import { INSERT_LAST_RUN_BLOCK_COMMAND } from '../last-run-block'
|
||||
|
||||
type ComponentPickerProps = {
|
||||
triggerString: string
|
||||
contextBlock?: ContextBlockType
|
||||
queryBlock?: QueryBlockType
|
||||
historyBlock?: HistoryBlockType
|
||||
variableBlock?: VariableBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
currentBlock?: CurrentBlockType
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
isSupportFileVar?: boolean
|
||||
}
|
||||
const ComponentPicker = ({
|
||||
triggerString,
|
||||
contextBlock,
|
||||
queryBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
isSupportFileVar,
|
||||
}: ComponentPickerProps) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const { refs, floatingStyles, isPositioned } = useFloating({
|
||||
placement: 'bottom-start',
|
||||
middleware: [
|
||||
offset(0), // fix hide cursor
|
||||
shift({
|
||||
padding: 8,
|
||||
}),
|
||||
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 {
|
||||
allFlattenOptions,
|
||||
workflowVariableOptions,
|
||||
} = useOptions(
|
||||
contextBlock,
|
||||
queryBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
currentBlock,
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
)
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(
|
||||
selectedOption: PickerBlockMenuOption,
|
||||
nodeToRemove: TextNode | null,
|
||||
closeMenu: () => void,
|
||||
) => {
|
||||
editor.update(() => {
|
||||
if (nodeToRemove && selectedOption?.key)
|
||||
nodeToRemove.remove()
|
||||
|
||||
selectedOption.onSelectMenuOption()
|
||||
closeMenu()
|
||||
})
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
|
||||
const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
|
||||
editor.update(() => {
|
||||
const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
|
||||
if (needRemove)
|
||||
needRemove.remove()
|
||||
})
|
||||
const isFlat = variables.length === 1
|
||||
if (isFlat) {
|
||||
const varName = variables[0]
|
||||
if (varName === 'current')
|
||||
editor.dispatchCommand(INSERT_CURRENT_BLOCK_COMMAND, currentBlock?.generatorType)
|
||||
else if (varName === 'error_message')
|
||||
editor.dispatchCommand(INSERT_ERROR_MESSAGE_BLOCK_COMMAND, null)
|
||||
else if (varName === 'last_run')
|
||||
editor.dispatchCommand(INSERT_LAST_RUN_BLOCK_COMMAND, null)
|
||||
}
|
||||
else 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, currentBlock?.generatorType, checkForTriggerMatch, triggerString])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' })
|
||||
editor.dispatchCommand(KEY_ESCAPE_COMMAND, escapeEvent)
|
||||
}, [editor])
|
||||
|
||||
const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>((
|
||||
anchorElementRef,
|
||||
{ options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
||||
) => {
|
||||
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
|
||||
return null
|
||||
|
||||
setTimeout(() => {
|
||||
if (anchorElementRef.current)
|
||||
refs.setReference(anchorElementRef.current)
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
ReactDOM.createPortal(
|
||||
// The `LexicalMenu` will try to calculate the position of the floating menu based on the first child.
|
||||
// Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected.
|
||||
// See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
|
||||
<div className='h-0 w-0'>
|
||||
<div
|
||||
className='w-[260px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'
|
||||
style={{
|
||||
...floatingStyles,
|
||||
visibility: isPositioned ? 'visible' : 'hidden',
|
||||
}}
|
||||
ref={refs.setFloating}
|
||||
>
|
||||
{
|
||||
workflowVariableBlock?.show && (
|
||||
<div className='p-1'>
|
||||
<VarReferenceVars
|
||||
searchBoxClassName='mt-1'
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
}}
|
||||
maxHeightClass='max-h-[34vh]'
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
showManageInputField={workflowVariableBlock.showManageInputField}
|
||||
onManageInputField={workflowVariableBlock.onManageInputField}
|
||||
autoFocus={false}
|
||||
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && !!options.length && (
|
||||
<div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
{
|
||||
options.map((option, index) => (
|
||||
<Fragment key={option.key}>
|
||||
{
|
||||
// Divider
|
||||
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||
<div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div>
|
||||
)
|
||||
}
|
||||
{option.renderMenuOption({
|
||||
queryString,
|
||||
isSelected: selectedIndex === index,
|
||||
onSelect: () => {
|
||||
selectOptionAndCleanUp(option)
|
||||
},
|
||||
onSetHighlight: () => {
|
||||
setHighlightedIndex(index)
|
||||
},
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}, [allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
options={allFlattenOptions}
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
// The `translate` class is used to workaround the issue that the `typeahead-menu` menu is not positioned as expected.
|
||||
// See also https://github.com/facebook/lexical/blob/772520509308e8ba7e4a82b6cd1996a78b3298d0/packages/lexical-react/src/shared/LexicalMenu.ts#L498
|
||||
//
|
||||
// We no need the position function of the `LexicalTypeaheadMenuPlugin`,
|
||||
// so the reference anchor should be positioned based on the range of the trigger string, and the menu will be positioned by the floating ui.
|
||||
anchorClassName='z-[999999] translate-y-[calc(-100%-3px)]'
|
||||
menuRenderFn={renderMenu}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ComponentPicker)
|
||||
@@ -0,0 +1,31 @@
|
||||
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import { Fragment } from 'react'
|
||||
|
||||
/**
|
||||
* Corresponds to the `MenuRenderFn` type from `@lexical/react/LexicalTypeaheadMenuPlugin`.
|
||||
*/
|
||||
type MenuOptionRenderProps = {
|
||||
isSelected: boolean
|
||||
onSelect: () => void
|
||||
onSetHighlight: () => void
|
||||
queryString: string | null
|
||||
}
|
||||
|
||||
export class PickerBlockMenuOption extends MenuOption {
|
||||
public group?: string
|
||||
|
||||
constructor(
|
||||
private data: {
|
||||
key: string
|
||||
group?: string
|
||||
onSelect?: () => void
|
||||
render: (menuRenderProps: MenuOptionRenderProps) => React.JSX.Element
|
||||
},
|
||||
) {
|
||||
super(data.key)
|
||||
this.group = data.group
|
||||
}
|
||||
|
||||
public onSelectMenuOption = () => this.data.onSelect?.()
|
||||
public renderMenuOption = (menuRenderProps: MenuOptionRenderProps) => <Fragment key={this.data.key}>{this.data.render(menuRenderProps)}</Fragment>
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { memo } from 'react'
|
||||
|
||||
type PromptMenuItemMenuItemProps = {
|
||||
icon: React.JSX.Element
|
||||
title: string
|
||||
disabled?: boolean
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
onMouseEnter: () => void
|
||||
setRefElement?: (element: HTMLDivElement) => void
|
||||
}
|
||||
export const PromptMenuItem = memo(({
|
||||
icon,
|
||||
title,
|
||||
disabled,
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
setRefElement,
|
||||
}: PromptMenuItemMenuItemProps) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex h-6 cursor-pointer items-center rounded-md px-3 hover:bg-state-base-hover
|
||||
${isSelected && !disabled && '!bg-state-base-hover'}
|
||||
${disabled ? 'cursor-not-allowed opacity-30' : 'cursor-pointer hover:bg-state-base-hover'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={setRefElement}
|
||||
onMouseEnter={() => {
|
||||
if (disabled)
|
||||
return
|
||||
onMouseEnter()
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onClick={() => {
|
||||
if (disabled)
|
||||
return
|
||||
onClick()
|
||||
}}>
|
||||
{icon}
|
||||
<div className='ml-1 text-[13px] text-text-secondary'>{title}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
PromptMenuItem.displayName = 'PromptMenuItem'
|
||||
@@ -0,0 +1,64 @@
|
||||
import { memo } from 'react'
|
||||
|
||||
type VariableMenuItemProps = {
|
||||
title: string
|
||||
icon?: React.JSX.Element
|
||||
extraElement?: React.JSX.Element
|
||||
isSelected: boolean
|
||||
queryString: string | null
|
||||
onClick: () => void
|
||||
onMouseEnter: () => void
|
||||
setRefElement?: (element: HTMLDivElement) => void
|
||||
}
|
||||
export const VariableMenuItem = memo(({
|
||||
title,
|
||||
icon,
|
||||
extraElement,
|
||||
isSelected,
|
||||
queryString,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
setRefElement,
|
||||
}: VariableMenuItemProps) => {
|
||||
let before = title
|
||||
let middle = ''
|
||||
let after = ''
|
||||
|
||||
if (queryString) {
|
||||
const regex = new RegExp(queryString, 'i')
|
||||
const match = regex.exec(title)
|
||||
|
||||
if (match) {
|
||||
before = title.substring(0, match.index)
|
||||
middle = match[0]
|
||||
after = title.substring(match.index + match[0].length)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex h-6 cursor-pointer items-center rounded-md px-3 hover:bg-state-base-hover
|
||||
${isSelected && 'bg-state-base-hover'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={setRefElement}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onClick={onClick}>
|
||||
<div className='mr-2'>
|
||||
{icon}
|
||||
</div>
|
||||
<div className='grow truncate text-[13px] text-text-secondary' title={title}>
|
||||
{before}
|
||||
<span className='text-text-accent'>{middle}</span>
|
||||
{after}
|
||||
</div>
|
||||
{extraElement}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
VariableMenuItem.displayName = 'VariableMenuItem'
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { useSelectOrDelete, useTrigger } from '../../hooks'
|
||||
import { UPDATE_DATASETS_EVENT_EMITTER } from '../../constants'
|
||||
import type { Dataset } from './index'
|
||||
import { DELETE_CONTEXT_BLOCK_COMMAND } from './index'
|
||||
import { File05, Folder } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
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)
|
||||
const [triggerRef, open, setOpen] = useTrigger()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [localDatasets, setLocalDatasets] = useState<Dataset[]>(datasets)
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v?.type === UPDATE_DATASETS_EVENT_EMITTER)
|
||||
setLocalDatasets(v.payload)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
group inline-flex h-6 items-center rounded-[5px] border border-transparent bg-[#F4F3FF] pl-1 pr-0.5 text-[#6938EF] hover:bg-[#EBE9FE]
|
||||
${open ? 'bg-[#EBE9FE]' : 'bg-[#F4F3FF]'}
|
||||
${isSelected && '!border-[#9B8AFB]'}
|
||||
`} ref={ref}>
|
||||
<File05 className='mr-1 h-[14px] w-[14px]' />
|
||||
<div className='mr-1 text-xs font-medium'>{t('common.promptEditor.context.item.title')}</div>
|
||||
{!canNotAddContext && (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 3,
|
||||
alignmentAxis: -147,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger ref={triggerRef}>
|
||||
<div className={`
|
||||
flex h-[18px] w-[18px] cursor-pointer items-center justify-center rounded text-[11px] font-semibold
|
||||
${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] rounded-xl bg-white 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 h-8 items-center'>
|
||||
<div className='mr-2 flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-[0.5px] border-[#EAECF5] bg-[#F5F8FF]'>
|
||||
<Folder className='h-4 w-4 text-[#444CE7]' />
|
||||
</div>
|
||||
<div className='truncate text-sm text-gray-800' title=''>{dataset.name}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='flex h-8 cursor-pointer items-center text-[#155EEF]' onClick={onAddContext}>
|
||||
<div className='mr-2 flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-[0.5px] border-gray-100'>
|
||||
<RiAddLine className='h-[14px] w-[14px]' />
|
||||
</div>
|
||||
<div className='text-[13px] font-medium' title=''>{t('common.promptEditor.context.modal.add')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-b-xl border-t-[0.5px] border-gray-50 bg-gray-50 px-4 py-3 text-xs text-gray-500'>
|
||||
{t('common.promptEditor.context.modal.footer')}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContextBlockComponent
|
||||
@@ -0,0 +1,64 @@
|
||||
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 type { ContextBlockType } from '../../types'
|
||||
import {
|
||||
$createContextBlockNode,
|
||||
ContextBlockNode,
|
||||
} from '../context-block/node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT)
|
||||
|
||||
const ContextBlockReplacementBlock = ({
|
||||
datasets = [],
|
||||
onAddContext = noop,
|
||||
onInsert,
|
||||
canNotAddContext,
|
||||
}: ContextBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ContextBlockNode]))
|
||||
throw new Error('ContextBlockNodePlugin: ContextBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createContextBlockNode = useCallback((): ContextBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext, canNotAddContext))
|
||||
}, [datasets, onAddContext, onInsert, canNotAddContext])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + CONTEXT_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)),
|
||||
)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(ContextBlockReplacementBlock)
|
||||
@@ -0,0 +1,75 @@
|
||||
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 { ContextBlockType } from '../../types'
|
||||
import {
|
||||
$createContextBlockNode,
|
||||
ContextBlockNode,
|
||||
} from './node'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
export const INSERT_CONTEXT_BLOCK_COMMAND = createCommand('INSERT_CONTEXT_BLOCK_COMMAND')
|
||||
export const DELETE_CONTEXT_BLOCK_COMMAND = createCommand('DELETE_CONTEXT_BLOCK_COMMAND')
|
||||
|
||||
export type Dataset = {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const ContextBlock = memo(({
|
||||
datasets = [],
|
||||
onAddContext = noop,
|
||||
onInsert,
|
||||
onDelete,
|
||||
canNotAddContext,
|
||||
}: ContextBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ContextBlockNode]))
|
||||
throw new Error('ContextBlockPlugin: ContextBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_CONTEXT_BLOCK_COMMAND,
|
||||
() => {
|
||||
const contextBlockNode = $createContextBlockNode(datasets, onAddContext, canNotAddContext)
|
||||
|
||||
$insertNodes([contextBlockNode])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_CONTEXT_BLOCK_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext])
|
||||
|
||||
return null
|
||||
})
|
||||
ContextBlock.displayName = 'ContextBlock'
|
||||
|
||||
export { ContextBlock }
|
||||
export { ContextBlockNode } from './node'
|
||||
export { default as ContextBlockReplacementBlock } from './context-block-replacement-block'
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import ContextBlockComponent from './component'
|
||||
import type { Dataset } from './index'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode & { datasets: Dataset[]; onAddContext: () => void; canNotAddContext: boolean }
|
||||
|
||||
export class ContextBlockNode extends DecoratorNode<React.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, node.getKey(), node.__canNotAddContext)
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(datasets: Dataset[], onAddContext: () => void, key?: NodeKey, canNotAddContext?: boolean) {
|
||||
super(key)
|
||||
|
||||
this.__datasets = datasets
|
||||
this.__onAddContext = onAddContext
|
||||
this.__canNotAddContext = canNotAddContext || false
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<ContextBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
datasets={this.getDatasets()}
|
||||
onAddContext={this.getOnAddContext()}
|
||||
canNotAddContext={this.getCanNotAddContext()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
getDatasets(): Dataset[] {
|
||||
const self = this.getLatest()
|
||||
|
||||
return self.__datasets
|
||||
}
|
||||
|
||||
getOnAddContext(): () => void {
|
||||
const self = this.getLatest()
|
||||
|
||||
return self.__onAddContext
|
||||
}
|
||||
|
||||
getCanNotAddContext(): boolean {
|
||||
const self = this.getLatest()
|
||||
|
||||
return self.__canNotAddContext
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedNode): ContextBlockNode {
|
||||
const node = $createContextBlockNode(serializedNode.datasets, serializedNode.onAddContext, serializedNode.canNotAddContext)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'context-block',
|
||||
version: 1,
|
||||
datasets: this.getDatasets(),
|
||||
onAddContext: this.getOnAddContext(),
|
||||
canNotAddContext: this.getCanNotAddContext(),
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#context#}}'
|
||||
}
|
||||
}
|
||||
export function $createContextBlockNode(datasets: Dataset[], onAddContext: () => void, canNotAddContext?: boolean): ContextBlockNode {
|
||||
return new ContextBlockNode(datasets, onAddContext, undefined, canNotAddContext)
|
||||
}
|
||||
|
||||
export function $isContextBlockNode(
|
||||
node: ContextBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof ContextBlockNode
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { CurrentBlockNode, DELETE_CURRENT_BLOCK_COMMAND } from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import { CodeAssistant, MagicEdit } from '../../../icons/src/vender/line/general'
|
||||
|
||||
type CurrentBlockComponentProps = {
|
||||
nodeKey: string
|
||||
generatorType: GeneratorType
|
||||
}
|
||||
|
||||
const CurrentBlockComponent: FC<CurrentBlockComponentProps> = ({
|
||||
nodeKey,
|
||||
generatorType,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CURRENT_BLOCK_COMMAND)
|
||||
|
||||
const Icon = generatorType === GeneratorType.prompt ? MagicEdit : CodeAssistant
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-violet-violet-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Icon className='mr-0.5 h-[14px] w-[14px]' />
|
||||
<div className='text-xs font-medium'>{generatorType === GeneratorType.prompt ? 'current_prompt' : 'current_code'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CurrentBlockComponent
|
||||
@@ -0,0 +1,61 @@
|
||||
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 { CURRENT_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { CurrentBlockType } from '../../types'
|
||||
import {
|
||||
$createCurrentBlockNode,
|
||||
CurrentBlockNode,
|
||||
} from './node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(CURRENT_PLACEHOLDER_TEXT)
|
||||
|
||||
const CurrentBlockReplacementBlock = ({
|
||||
generatorType,
|
||||
onInsert,
|
||||
}: CurrentBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('CurrentBlockNodePlugin: CurrentBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createCurrentBlockNode = useCallback((): CurrentBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createCurrentBlockNode(generatorType))
|
||||
}, [onInsert, generatorType])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + CURRENT_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)),
|
||||
)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(CurrentBlockReplacementBlock)
|
||||
@@ -0,0 +1,66 @@
|
||||
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 { CurrentBlockType } from '../../types'
|
||||
import {
|
||||
$createCurrentBlockNode,
|
||||
CurrentBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_CURRENT_BLOCK_COMMAND = createCommand('INSERT_CURRENT_BLOCK_COMMAND')
|
||||
export const DELETE_CURRENT_BLOCK_COMMAND = createCommand('DELETE_CURRENT_BLOCK_COMMAND')
|
||||
|
||||
const CurrentBlock = memo(({
|
||||
generatorType,
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: CurrentBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([CurrentBlockNode]))
|
||||
throw new Error('CURRENTBlockPlugin: CURRENTBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_CURRENT_BLOCK_COMMAND,
|
||||
() => {
|
||||
const currentBlockNode = $createCurrentBlockNode(generatorType)
|
||||
|
||||
$insertNodes([currentBlockNode])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_CURRENT_BLOCK_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, generatorType, onDelete, onInsert])
|
||||
|
||||
return null
|
||||
})
|
||||
CurrentBlock.displayName = 'CurrentBlock'
|
||||
|
||||
export { CurrentBlock }
|
||||
export { CurrentBlockNode } from './node'
|
||||
export { default as CurrentBlockReplacementBlock } from './current-block-replacement-block'
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import CurrentBlockComponent from './component'
|
||||
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode & { generatorType: GeneratorType; }
|
||||
|
||||
export class CurrentBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
__generatorType: GeneratorType
|
||||
static getType(): string {
|
||||
return 'current-block'
|
||||
}
|
||||
|
||||
static clone(node: CurrentBlockNode): CurrentBlockNode {
|
||||
return new CurrentBlockNode(node.__generatorType, node.getKey())
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(generatorType: GeneratorType, key?: NodeKey) {
|
||||
super(key)
|
||||
|
||||
this.__generatorType = generatorType
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<CurrentBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
generatorType={this.getGeneratorType()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
getGeneratorType(): GeneratorType {
|
||||
const self = this.getLatest()
|
||||
return self.__generatorType
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedNode): CurrentBlockNode {
|
||||
const node = $createCurrentBlockNode(serializedNode.generatorType)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'current-block',
|
||||
version: 1,
|
||||
generatorType: this.getGeneratorType(),
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#current#}}'
|
||||
}
|
||||
}
|
||||
export function $createCurrentBlockNode(type: GeneratorType): CurrentBlockNode {
|
||||
return new CurrentBlockNode(type)
|
||||
}
|
||||
|
||||
export function $isCurrentBlockNode(
|
||||
node: CurrentBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof CurrentBlockNode
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { EditorConfig, SerializedTextNode } from 'lexical'
|
||||
import { $createTextNode, TextNode } from 'lexical'
|
||||
|
||||
export class CustomTextNode extends TextNode {
|
||||
static getType() {
|
||||
return 'custom-text'
|
||||
}
|
||||
|
||||
static clone(node: CustomTextNode) {
|
||||
return new CustomTextNode(node.__text, node.__key)
|
||||
}
|
||||
|
||||
// constructor(text: string, key?: NodeKey) {
|
||||
// super(text, key)
|
||||
// }
|
||||
|
||||
createDOM(config: EditorConfig) {
|
||||
const dom = super.createDOM(config)
|
||||
return dom
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTextNode): TextNode {
|
||||
const node = $createTextNode(serializedNode.text)
|
||||
node.setFormat(serializedNode.format)
|
||||
node.setDetail(serializedNode.detail)
|
||||
node.setMode(serializedNode.mode)
|
||||
node.setStyle(serializedNode.style)
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTextNode {
|
||||
return {
|
||||
detail: this.getDetail(),
|
||||
format: this.getFormat(),
|
||||
mode: this.getMode(),
|
||||
style: this.getStyle(),
|
||||
text: this.getTextContent(),
|
||||
type: 'custom-text',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
isSimpleText() {
|
||||
return (
|
||||
(this.__type === 'text' || this.__type === 'custom-text') && this.__mode === 0)
|
||||
}
|
||||
}
|
||||
|
||||
export function $createCustomTextNode(text: string): CustomTextNode {
|
||||
return new CustomTextNode(text)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { DELETE_ERROR_MESSAGE_COMMAND, ErrorMessageBlockNode } from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Variable02 } from '../../../icons/src/vender/solid/development'
|
||||
|
||||
type Props = {
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
const ErrorMessageBlockComponent: FC<Props> = ({
|
||||
nodeKey,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_ERROR_MESSAGE_COMMAND)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-orange-dark-orange-dark-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
|
||||
<div className='text-xs font-medium'>error_message</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorMessageBlockComponent
|
||||
@@ -0,0 +1,60 @@
|
||||
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 { ERROR_MESSAGE_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { ErrorMessageBlockType } from '../../types'
|
||||
import {
|
||||
$createErrorMessageBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
} from './node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(ERROR_MESSAGE_PLACEHOLDER_TEXT)
|
||||
|
||||
const ErrorMessageBlockReplacementBlock = ({
|
||||
onInsert,
|
||||
}: ErrorMessageBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('ErrorMessageBlockNodePlugin: ErrorMessageBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createErrorMessageBlockNode = useCallback((): ErrorMessageBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createErrorMessageBlockNode())
|
||||
}, [onInsert])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + ERROR_MESSAGE_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createErrorMessageBlockNode)),
|
||||
)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(ErrorMessageBlockReplacementBlock)
|
||||
@@ -0,0 +1,65 @@
|
||||
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 { ErrorMessageBlockType } from '../../types'
|
||||
import {
|
||||
$createErrorMessageBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_ERROR_MESSAGE_BLOCK_COMMAND = createCommand('INSERT_ERROR_MESSAGE_BLOCK_COMMAND')
|
||||
export const DELETE_ERROR_MESSAGE_COMMAND = createCommand('DELETE_ERROR_MESSAGE_COMMAND')
|
||||
|
||||
const ErrorMessageBlock = memo(({
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: ErrorMessageBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ErrorMessageBlockNode]))
|
||||
throw new Error('ERROR_MESSAGEBlockPlugin: ERROR_MESSAGEBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_ERROR_MESSAGE_BLOCK_COMMAND,
|
||||
() => {
|
||||
const Node = $createErrorMessageBlockNode()
|
||||
|
||||
$insertNodes([Node])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_ERROR_MESSAGE_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onDelete, onInsert])
|
||||
|
||||
return null
|
||||
})
|
||||
ErrorMessageBlock.displayName = 'ErrorMessageBlock'
|
||||
|
||||
export { ErrorMessageBlock }
|
||||
export { ErrorMessageBlockNode } from './node'
|
||||
export { default as ErrorMessageBlockReplacementBlock } from './error-message-block-replacement-block'
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import ErrorMessageBlockComponent from './component'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode
|
||||
|
||||
export class ErrorMessageBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
static getType(): string {
|
||||
return 'error-message-block'
|
||||
}
|
||||
|
||||
static clone(node: ErrorMessageBlockNode): ErrorMessageBlockNode {
|
||||
return new ErrorMessageBlockNode(node.getKey())
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key)
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<ErrorMessageBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
static importJSON(): ErrorMessageBlockNode {
|
||||
const node = $createErrorMessageBlockNode()
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'error-message-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#error_message#}}'
|
||||
}
|
||||
}
|
||||
export function $createErrorMessageBlockNode(): ErrorMessageBlockNode {
|
||||
return new ErrorMessageBlockNode()
|
||||
}
|
||||
|
||||
export function $isErrorMessageBlockNode(
|
||||
node: ErrorMessageBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof ErrorMessageBlockNode
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiMoreFill,
|
||||
} from '@remixicon/react'
|
||||
import { useSelectOrDelete, useTrigger } from '../../hooks'
|
||||
import { UPDATE_HISTORY_EVENT_EMITTER } from '../../constants'
|
||||
import type { RoleName } from './index'
|
||||
import { DELETE_HISTORY_BLOCK_COMMAND } from './index'
|
||||
import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
type HistoryBlockComponentProps = {
|
||||
nodeKey: string
|
||||
roleName?: RoleName
|
||||
onEditRole: () => void
|
||||
}
|
||||
|
||||
const HistoryBlockComponent: FC<HistoryBlockComponentProps> = ({
|
||||
nodeKey,
|
||||
roleName = { user: '', assistant: '' },
|
||||
onEditRole,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HISTORY_BLOCK_COMMAND)
|
||||
const [triggerRef, open, setOpen] = useTrigger()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [localRoleName, setLocalRoleName] = useState<RoleName>(roleName)
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v?.type === UPDATE_HISTORY_EVENT_EMITTER)
|
||||
setLocalRoleName(v.payload)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
group inline-flex h-6 items-center rounded-[5px] border border-transparent pl-1 pr-0.5 text-[#DD2590] hover:bg-[#FCE7F6]
|
||||
${open ? 'bg-[#FCE7F6]' : 'bg-[#FDF2FA]'}
|
||||
${isSelected && '!border-[#F670C7]'}
|
||||
`} ref={ref}>
|
||||
<MessageClockCircle className='mr-1 h-[14px] w-[14px]' />
|
||||
<div className='mr-1 text-xs font-medium'>{t('common.promptEditor.history.item.title')}</div>
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='top-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
alignmentAxis: -148,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger ref={triggerRef}>
|
||||
<div className={`
|
||||
flex h-[18px] w-[18px] cursor-pointer items-center justify-center rounded
|
||||
${open ? 'bg-[#DD2590] text-white' : 'bg-white/50 group-hover:bg-white group-hover:shadow-xs'}
|
||||
`}>
|
||||
<RiMoreFill className='h-3 w-3' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 100 }}>
|
||||
<div className='w-[360px] rounded-xl bg-white shadow-lg'>
|
||||
<div className='p-4'>
|
||||
<div className='mb-2 text-xs font-medium text-gray-500'>{t('common.promptEditor.history.modal.title')}</div>
|
||||
<div className='flex items-center text-sm text-gray-700'>
|
||||
<div className='mr-1 w-20 text-xs font-semibold'>{localRoleName?.user}</div>
|
||||
{t('common.promptEditor.history.modal.user')}
|
||||
</div>
|
||||
<div className='flex items-center text-sm text-gray-700'>
|
||||
<div className='mr-1 w-20 text-xs font-semibold'>{localRoleName?.assistant}</div>
|
||||
{t('common.promptEditor.history.modal.assistant')}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='cursor-pointer rounded-b-xl border-t border-black/5 px-4 py-3 text-xs text-[#155EEF]'
|
||||
onClick={onEditRole}
|
||||
>
|
||||
{t('common.promptEditor.history.modal.edit')}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistoryBlockComponent
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
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 type { HistoryBlockType } from '../../types'
|
||||
import {
|
||||
$createHistoryBlockNode,
|
||||
HistoryBlockNode,
|
||||
} from '../history-block/node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
|
||||
|
||||
const HistoryBlockReplacementBlock = ({
|
||||
history = { user: '', assistant: '' },
|
||||
onEditRole = noop,
|
||||
onInsert,
|
||||
}: HistoryBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([HistoryBlockNode]))
|
||||
throw new Error('HistoryBlockNodePlugin: HistoryBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createHistoryBlockNode = useCallback((): HistoryBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createHistoryBlockNode(history, onEditRole))
|
||||
}, [history, onEditRole, onInsert])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + HISTORY_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)),
|
||||
)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default HistoryBlockReplacementBlock
|
||||
@@ -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 { HistoryBlockType } from '../../types'
|
||||
import {
|
||||
$createHistoryBlockNode,
|
||||
HistoryBlockNode,
|
||||
} from './node'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
export const INSERT_HISTORY_BLOCK_COMMAND = createCommand('INSERT_HISTORY_BLOCK_COMMAND')
|
||||
export const DELETE_HISTORY_BLOCK_COMMAND = createCommand('DELETE_HISTORY_BLOCK_COMMAND')
|
||||
|
||||
export type RoleName = {
|
||||
user: string
|
||||
assistant: string
|
||||
}
|
||||
|
||||
export type HistoryBlockProps = {
|
||||
roleName: RoleName
|
||||
onEditRole: () => void
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
const HistoryBlock = memo(({
|
||||
history = { user: '', assistant: '' },
|
||||
onEditRole = noop,
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: HistoryBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([HistoryBlockNode]))
|
||||
throw new Error('HistoryBlockPlugin: HistoryBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_HISTORY_BLOCK_COMMAND,
|
||||
() => {
|
||||
const historyBlockNode = $createHistoryBlockNode(history, onEditRole)
|
||||
|
||||
$insertNodes([historyBlockNode])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_HISTORY_BLOCK_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, history, onEditRole, onInsert, onDelete])
|
||||
|
||||
return null
|
||||
})
|
||||
HistoryBlock.displayName = 'HistoryBlock'
|
||||
|
||||
export { HistoryBlock }
|
||||
export { HistoryBlockNode } from './node'
|
||||
export { default as HistoryBlockReplacementBlock } from './history-block-replacement-block'
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import HistoryBlockComponent from './component'
|
||||
import type { RoleName } from './index'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode & { roleName: RoleName; onEditRole: () => void }
|
||||
|
||||
export class HistoryBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
__roleName: RoleName
|
||||
__onEditRole: () => void
|
||||
|
||||
static getType(): string {
|
||||
return 'history-block'
|
||||
}
|
||||
|
||||
static clone(node: HistoryBlockNode): HistoryBlockNode {
|
||||
return new HistoryBlockNode(node.__roleName, node.__onEditRole, node.__key)
|
||||
}
|
||||
|
||||
constructor(roleName: RoleName, onEditRole: () => void, key?: NodeKey) {
|
||||
super(key)
|
||||
|
||||
this.__roleName = roleName
|
||||
this.__onEditRole = onEditRole
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<HistoryBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
roleName={this.getRoleName()}
|
||||
onEditRole={this.getOnEditRole()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
getRoleName(): RoleName {
|
||||
const self = this.getLatest()
|
||||
|
||||
return self.__roleName
|
||||
}
|
||||
|
||||
getOnEditRole(): () => void {
|
||||
const self = this.getLatest()
|
||||
|
||||
return self.__onEditRole
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedNode): HistoryBlockNode {
|
||||
const node = $createHistoryBlockNode(serializedNode.roleName, serializedNode.onEditRole)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'history-block',
|
||||
version: 1,
|
||||
roleName: this.getRoleName(),
|
||||
onEditRole: this.getOnEditRole,
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#histories#}}'
|
||||
}
|
||||
}
|
||||
export function $createHistoryBlockNode(roleName: RoleName, onEditRole: () => void): HistoryBlockNode {
|
||||
return new HistoryBlockNode(roleName, onEditRole)
|
||||
}
|
||||
|
||||
export function $isHistoryBlockNode(
|
||||
node: HistoryBlockNode | LexicalNode | null | undefined,
|
||||
): node is HistoryBlockNode {
|
||||
return node instanceof HistoryBlockNode
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { DELETE_LAST_RUN_COMMAND, LastRunBlockNode } from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Variable02 } from '../../../icons/src/vender/solid/development'
|
||||
|
||||
type Props = {
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
const LastRunBlockComponent: FC<Props> = ({
|
||||
nodeKey,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_LAST_RUN_COMMAND)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-text-accent hover:border-state-accent-solid hover:bg-state-accent-hover',
|
||||
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
|
||||
<div className='text-xs font-medium'>last_run</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LastRunBlockComponent
|
||||
@@ -0,0 +1,65 @@
|
||||
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 { LastRunBlockType } from '../../types'
|
||||
import {
|
||||
$createLastRunBlockNode,
|
||||
LastRunBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_LAST_RUN_BLOCK_COMMAND = createCommand('INSERT_LAST_RUN_BLOCK_COMMAND')
|
||||
export const DELETE_LAST_RUN_COMMAND = createCommand('DELETE_LAST_RUN_COMMAND')
|
||||
|
||||
const LastRunBlock = memo(({
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: LastRunBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('Last_RunBlockPlugin: Last_RunBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_LAST_RUN_BLOCK_COMMAND,
|
||||
() => {
|
||||
const Node = $createLastRunBlockNode()
|
||||
|
||||
$insertNodes([Node])
|
||||
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_LAST_RUN_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onDelete, onInsert])
|
||||
|
||||
return null
|
||||
})
|
||||
LastRunBlock.displayName = 'LastRunBlock'
|
||||
|
||||
export { LastRunBlock }
|
||||
export { LastRunBlockNode } from './node'
|
||||
export { default as LastRunReplacementBlock } from './last-run-block-replacement-block'
|
||||
@@ -0,0 +1,60 @@
|
||||
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 { LAST_RUN_PLACEHOLDER_TEXT } from '../../constants'
|
||||
import type { LastRunBlockType } from '../../types'
|
||||
import {
|
||||
$createLastRunBlockNode,
|
||||
LastRunBlockNode,
|
||||
} from './node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(LAST_RUN_PLACEHOLDER_TEXT)
|
||||
|
||||
const LastRunReplacementBlock = ({
|
||||
onInsert,
|
||||
}: LastRunBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([LastRunBlockNode]))
|
||||
throw new Error('LastRunMessageBlockNodePlugin: LastRunMessageBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createLastRunBlockNode = useCallback((): LastRunBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createLastRunBlockNode())
|
||||
}, [onInsert])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + LAST_RUN_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)),
|
||||
)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(LastRunReplacementBlock)
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import LastRunBlockComponent from './component'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode
|
||||
|
||||
export class LastRunBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
static getType(): string {
|
||||
return 'last-run-block'
|
||||
}
|
||||
|
||||
static clone(node: LastRunBlockNode): LastRunBlockNode {
|
||||
return new LastRunBlockNode(node.getKey())
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key)
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<LastRunBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
static importJSON(): LastRunBlockNode {
|
||||
const node = $createLastRunBlockNode()
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'last-run-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#last_run#}}'
|
||||
}
|
||||
}
|
||||
export function $createLastRunBlockNode(): LastRunBlockNode {
|
||||
return new LastRunBlockNode()
|
||||
}
|
||||
|
||||
export function $isLastRunBlockNode(
|
||||
node: LastRunBlockNode | LexicalNode | null | undefined,
|
||||
): boolean {
|
||||
return node instanceof LastRunBlockNode
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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,
|
||||
(event) => {
|
||||
// Check if the clicked target element is var-search-input
|
||||
const target = event?.relatedTarget as HTMLElement
|
||||
if (!target?.classList?.contains('var-search-input')) {
|
||||
ref.current = setTimeout(() => {
|
||||
editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' }))
|
||||
}, 200)
|
||||
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
|
||||
@@ -0,0 +1,28 @@
|
||||
import { memo } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const Placeholder = ({
|
||||
compact,
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
compact?: boolean
|
||||
value?: ReactNode
|
||||
className?: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'pointer-events-none absolute left-0 top-0 h-full w-full select-none text-sm text-components-input-text-placeholder',
|
||||
compact ? 'text-[13px] leading-5' : 'text-sm leading-6',
|
||||
className,
|
||||
)}>
|
||||
{value || t('common.promptEditor.placeholder')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Placeholder)
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelectOrDelete } from '../../hooks'
|
||||
import { DELETE_QUERY_BLOCK_COMMAND } from './index'
|
||||
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
|
||||
|
||||
type QueryBlockComponentProps = {
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
const QueryBlockComponent: FC<QueryBlockComponentProps> = ({
|
||||
nodeKey,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_QUERY_BLOCK_COMMAND)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
inline-flex h-6 items-center rounded-[5px] border border-transparent bg-[#FFF6ED] pl-1 pr-0.5 hover:bg-[#FFEAD5]
|
||||
${isSelected && '!border-[#FD853A]'}
|
||||
`}
|
||||
ref={ref}
|
||||
>
|
||||
<UserEdit02 className='mr-1 h-[14px] w-[14px] text-[#FD853A]' />
|
||||
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'{{'}</div>
|
||||
<div className='text-xs font-medium text-[#EC4A0A]'>{t('common.promptEditor.query.item.title')}</div>
|
||||
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'}}'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QueryBlockComponent
|
||||
@@ -0,0 +1,68 @@
|
||||
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 { QueryBlockType } from '../../types'
|
||||
import {
|
||||
$createQueryBlockNode,
|
||||
QueryBlockNode,
|
||||
} from './node'
|
||||
|
||||
export const INSERT_QUERY_BLOCK_COMMAND = createCommand('INSERT_QUERY_BLOCK_COMMAND')
|
||||
export const DELETE_QUERY_BLOCK_COMMAND = createCommand('DELETE_QUERY_BLOCK_COMMAND')
|
||||
|
||||
export type QueryBlockProps = {
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
const QueryBlock = memo(({
|
||||
onInsert,
|
||||
onDelete,
|
||||
}: QueryBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([QueryBlockNode]))
|
||||
throw new Error('QueryBlockPlugin: QueryBlock not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_QUERY_BLOCK_COMMAND,
|
||||
() => {
|
||||
const contextBlockNode = $createQueryBlockNode()
|
||||
|
||||
$insertNodes([contextBlockNode])
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_QUERY_BLOCK_COMMAND,
|
||||
() => {
|
||||
if (onDelete)
|
||||
onDelete()
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor, onInsert, onDelete])
|
||||
|
||||
return null
|
||||
})
|
||||
QueryBlock.displayName = 'QueryBlock'
|
||||
|
||||
export { QueryBlock }
|
||||
export { QueryBlockNode } from './node'
|
||||
export { default as QueryBlockReplacementBlock } from './query-block-replacement-block'
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { LexicalNode, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import QueryBlockComponent from './component'
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode
|
||||
|
||||
export class QueryBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
static getType(): string {
|
||||
return 'query-block'
|
||||
}
|
||||
|
||||
static clone(): QueryBlockNode {
|
||||
return new QueryBlockNode()
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return <QueryBlockComponent nodeKey={this.getKey()} />
|
||||
}
|
||||
|
||||
static importJSON(): QueryBlockNode {
|
||||
const node = $createQueryBlockNode()
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'query-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return '{{#query#}}'
|
||||
}
|
||||
}
|
||||
export function $createQueryBlockNode(): QueryBlockNode {
|
||||
return new QueryBlockNode()
|
||||
}
|
||||
|
||||
export function $isQueryBlockNode(
|
||||
node: QueryBlockNode | LexicalNode | null | undefined,
|
||||
): node is QueryBlockNode {
|
||||
return node instanceof QueryBlockNode
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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 type { QueryBlockType } from '../../types'
|
||||
import {
|
||||
$createQueryBlockNode,
|
||||
QueryBlockNode,
|
||||
} from '../query-block/node'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT)
|
||||
|
||||
const QueryBlockReplacementBlock = ({
|
||||
onInsert,
|
||||
}: QueryBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([QueryBlockNode]))
|
||||
throw new Error('QueryBlockNodePlugin: QueryBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createQueryBlockNode = useCallback((): QueryBlockNode => {
|
||||
if (onInsert)
|
||||
onInsert()
|
||||
return $applyNodeReplacement($createQueryBlockNode())
|
||||
}, [onInsert])
|
||||
|
||||
const getMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + QUERY_PLACEHOLDER_TEXT.length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
REGEX.lastIndex = 0
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)),
|
||||
)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(QueryBlockReplacementBlock)
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { TreeView } from '@lexical/react/LexicalTreeView'
|
||||
|
||||
const TreeViewPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
return (
|
||||
<TreeView
|
||||
viewClassName="tree-view-output"
|
||||
treeTypeButtonClassName="debug-treetype-button"
|
||||
timeTravelPanelClassName="debug-timetravel-panel"
|
||||
timeTravelButtonClassName="debug-timetravel-button"
|
||||
timeTravelPanelSliderClassName="debug-timetravel-panel-slider"
|
||||
timeTravelPanelButtonClassName="debug-timetravel-panel-button"
|
||||
editor={editor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TreeViewPlugin
|
||||
@@ -0,0 +1,42 @@
|
||||
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'
|
||||
|
||||
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 && 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
|
||||
}
|
||||
|
||||
export default UpdateBlock
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
|
||||
export const INSERT_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_BLOCK_COMMAND')
|
||||
export const INSERT_VARIABLE_VALUE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_VALUE_BLOCK_COMMAND')
|
||||
|
||||
const VariableBlock = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_VARIABLE_BLOCK_COMMAND,
|
||||
() => {
|
||||
const textNode = new CustomTextNode('{')
|
||||
$insertNodes([textNode])
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
INSERT_VARIABLE_VALUE_BLOCK_COMMAND,
|
||||
(value: string) => {
|
||||
const textNode = new CustomTextNode(value)
|
||||
$insertNodes([textNode])
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default VariableBlock
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import type { TextNode } from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useLexicalTextEntity } from '../../hooks'
|
||||
import {
|
||||
$createVariableValueBlockNode,
|
||||
VariableValueBlockNode,
|
||||
} from './node'
|
||||
import { getHashtagRegexString } from './utils'
|
||||
|
||||
const REGEX = new RegExp(getHashtagRegexString(), 'i')
|
||||
|
||||
const VariableValueBlock = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([VariableValueBlockNode]))
|
||||
throw new Error('VariableValueBlockPlugin: VariableValueNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createVariableValueBlockNode = useCallback((textNode: TextNode): VariableValueBlockNode => {
|
||||
return $createVariableValueBlockNode(textNode.getTextContent())
|
||||
}, [])
|
||||
|
||||
const getVariableValueMatch = useCallback((text: string) => {
|
||||
const matchArr = REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const hashtagLength = matchArr[0].length
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + hashtagLength
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useLexicalTextEntity<VariableValueBlockNode>(
|
||||
getVariableValueMatch,
|
||||
VariableValueBlockNode,
|
||||
createVariableValueBlockNode,
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default VariableValueBlock
|
||||
@@ -0,0 +1,64 @@
|
||||
import type {
|
||||
EditorConfig,
|
||||
LexicalNode,
|
||||
SerializedTextNode,
|
||||
} from 'lexical'
|
||||
import {
|
||||
$applyNodeReplacement,
|
||||
TextNode,
|
||||
} from 'lexical'
|
||||
|
||||
export class VariableValueBlockNode extends TextNode {
|
||||
static getType(): string {
|
||||
return 'variable-value-block'
|
||||
}
|
||||
|
||||
static clone(node: VariableValueBlockNode): VariableValueBlockNode {
|
||||
return new VariableValueBlockNode(node.__text, node.__key)
|
||||
}
|
||||
|
||||
// constructor(text: string, key?: NodeKey) {
|
||||
// super(text, key)
|
||||
// }
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const element = super.createDOM(config)
|
||||
element.classList.add('inline-flex', 'items-center', 'px-0.5', 'h-[22px]', 'text-text-accent', 'rounded-[5px]', 'align-middle')
|
||||
return element
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTextNode): TextNode {
|
||||
const node = $createVariableValueBlockNode(serializedNode.text)
|
||||
node.setFormat(serializedNode.format)
|
||||
node.setDetail(serializedNode.detail)
|
||||
node.setMode(serializedNode.mode)
|
||||
node.setStyle(serializedNode.style)
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTextNode {
|
||||
return {
|
||||
detail: this.getDetail(),
|
||||
format: this.getFormat(),
|
||||
mode: this.getMode(),
|
||||
style: this.getStyle(),
|
||||
text: this.getTextContent(),
|
||||
type: 'variable-value-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
canInsertTextBefore(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function $createVariableValueBlockNode(text = ''): VariableValueBlockNode {
|
||||
return $applyNodeReplacement(new VariableValueBlockNode(text))
|
||||
}
|
||||
|
||||
export function $isVariableValueNodeBlock(
|
||||
node: LexicalNode | null | undefined,
|
||||
): node is VariableValueBlockNode {
|
||||
return node instanceof VariableValueBlockNode
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function getHashtagRegexString(): string {
|
||||
const hashtag = '\\{\\{[a-zA-Z_][a-zA-Z0-9_]{0,29}\\}\\}'
|
||||
|
||||
return hashtag
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
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 { useReactFlow, useStoreApi } from 'reactflow'
|
||||
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 { isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import {
|
||||
VariableLabelInEditor,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
|
||||
type WorkflowVariableBlockComponentProps = {
|
||||
nodeKey: string
|
||||
variables: string[]
|
||||
workflowNodesMap: WorkflowNodesMap
|
||||
environmentVariables?: Var[]
|
||||
conversationVariables?: Var[]
|
||||
ragVariables?: Var[]
|
||||
getVarType?: (payload: {
|
||||
nodeId: string,
|
||||
valueSelector: ValueSelector,
|
||||
}) => Type
|
||||
}
|
||||
|
||||
const WorkflowVariableBlockComponent = ({
|
||||
nodeKey,
|
||||
variables,
|
||||
workflowNodesMap = {},
|
||||
getVarType,
|
||||
environmentVariables,
|
||||
conversationVariables,
|
||||
ragVariables,
|
||||
}: WorkflowVariableBlockComponentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND)
|
||||
const variablesLength = variables.length
|
||||
const isRagVar = isRagVariableVar(variables)
|
||||
const isShowAPart = variablesLength > 2 && !isRagVar
|
||||
const varName = (
|
||||
() => {
|
||||
const isSystem = isSystemVar(variables)
|
||||
const varName = variables[variablesLength - 1]
|
||||
return `${isSystem ? 'sys.' : ''}${varName}`
|
||||
}
|
||||
)()
|
||||
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
|
||||
const node = localWorkflowNodesMap![variables[isRagVar ? 1 : 0]]
|
||||
|
||||
const isException = isExceptionVariable(varName, node?.type)
|
||||
const variableValid = useMemo(() => {
|
||||
let variableValid = true
|
||||
const isEnv = isENV(variables)
|
||||
const isChatVar = isConversationVar(variables)
|
||||
const isGlobal = isGlobalVar(variables)
|
||||
if (isGlobal)
|
||||
return true
|
||||
|
||||
if (isEnv) {
|
||||
if (environmentVariables)
|
||||
variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
|
||||
}
|
||||
else if (isChatVar) {
|
||||
if (conversationVariables)
|
||||
variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`)
|
||||
}
|
||||
else if (isRagVar) {
|
||||
if (ragVariables)
|
||||
variableValid = ragVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}.${variables?.[2] ?? ''}`)
|
||||
}
|
||||
else {
|
||||
variableValid = !!node
|
||||
}
|
||||
return variableValid
|
||||
}, [variables, node, environmentVariables, conversationVariables, isRagVar, ragVariables])
|
||||
|
||||
const reactflow = useReactFlow()
|
||||
const store = useStoreApi()
|
||||
|
||||
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 handleVariableJump = useCallback(() => {
|
||||
const workflowContainer = document.getElementById('workflow-container')
|
||||
const {
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
} = workflowContainer!
|
||||
|
||||
const {
|
||||
setViewport,
|
||||
} = reactflow
|
||||
const { transform } = store.getState()
|
||||
const zoom = transform[2]
|
||||
const position = node.position
|
||||
setViewport({
|
||||
x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom,
|
||||
y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom,
|
||||
zoom: transform[2],
|
||||
})
|
||||
}, [node, reactflow, store])
|
||||
|
||||
const Item = (
|
||||
<VariableLabelInEditor
|
||||
nodeType={node?.type}
|
||||
nodeTitle={node?.title}
|
||||
variables={variables}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleVariableJump()
|
||||
}}
|
||||
isExceptionVariable={isException}
|
||||
errorMsg={!variableValid ? t('workflow.errorMsg.invalidVariable') : undefined}
|
||||
isSelected={isSelected}
|
||||
ref={ref}
|
||||
notShowFullPath={isShowAPart}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!node)
|
||||
return Item
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
noDecoration
|
||||
popupContent={
|
||||
<VarFullPathPanel
|
||||
nodeName={node.title}
|
||||
path={variables.slice(1)}
|
||||
varType={getVarType ? getVarType({
|
||||
nodeId: variables[0],
|
||||
valueSelector: variables,
|
||||
}) : Type.string}
|
||||
nodeType={node?.type}
|
||||
/>}
|
||||
disabled={!isShowAPart}
|
||||
>
|
||||
<div>{Item}</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(WorkflowVariableBlockComponent)
|
||||
@@ -0,0 +1,82 @@
|
||||
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 { GetVarType, 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
|
||||
getVarType: GetVarType
|
||||
}
|
||||
const WorkflowVariableBlock = memo(({
|
||||
workflowNodesMap,
|
||||
onInsert,
|
||||
onDelete,
|
||||
getVarType,
|
||||
}: 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, getVarType)
|
||||
|
||||
$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, getVarType])
|
||||
|
||||
return null
|
||||
})
|
||||
WorkflowVariableBlock.displayName = 'WorkflowVariableBlock'
|
||||
|
||||
export { WorkflowVariableBlock }
|
||||
export { WorkflowVariableBlockNode } from './node'
|
||||
export { default as WorkflowVariableBlockReplacementBlock } from './workflow-variable-block-replacement-block'
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import type { WorkflowVariableBlockType } from '../../types'
|
||||
import WorkflowVariableBlockComponent from './component'
|
||||
import type { GetVarType } from '../../types'
|
||||
import type { Var } from '@/app/components/workflow/types'
|
||||
|
||||
export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap']
|
||||
|
||||
export type SerializedNode = SerializedLexicalNode & {
|
||||
variables: string[]
|
||||
workflowNodesMap: WorkflowNodesMap
|
||||
getVarType?: GetVarType
|
||||
environmentVariables?: Var[]
|
||||
conversationVariables?: Var[]
|
||||
ragVariables?: Var[]
|
||||
}
|
||||
|
||||
export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
__variables: string[]
|
||||
__workflowNodesMap: WorkflowNodesMap
|
||||
__getVarType?: GetVarType
|
||||
__environmentVariables?: Var[]
|
||||
__conversationVariables?: Var[]
|
||||
__ragVariables?: Var[]
|
||||
|
||||
static getType(): string {
|
||||
return 'workflow-variable-block'
|
||||
}
|
||||
|
||||
static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
|
||||
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__environmentVariables, node.__conversationVariables, node.__ragVariables)
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any, key?: NodeKey, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[]) {
|
||||
super(key)
|
||||
|
||||
this.__variables = variables
|
||||
this.__workflowNodesMap = workflowNodesMap
|
||||
this.__getVarType = getVarType
|
||||
this.__environmentVariables = environmentVariables
|
||||
this.__conversationVariables = conversationVariables
|
||||
this.__ragVariables = ragVariables
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return div
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<WorkflowVariableBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
variables={this.__variables}
|
||||
workflowNodesMap={this.__workflowNodesMap}
|
||||
getVarType={this.__getVarType!}
|
||||
environmentVariables={this.__environmentVariables}
|
||||
conversationVariables={this.__conversationVariables}
|
||||
ragVariables={this.__ragVariables}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
|
||||
const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.environmentVariables, serializedNode.conversationVariables, serializedNode.ragVariables)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
exportJSON(): SerializedNode {
|
||||
return {
|
||||
type: 'workflow-variable-block',
|
||||
version: 1,
|
||||
variables: this.getVariables(),
|
||||
workflowNodesMap: this.getWorkflowNodesMap(),
|
||||
getVarType: this.getVarType(),
|
||||
environmentVariables: this.getEnvironmentVariables(),
|
||||
conversationVariables: this.getConversationVariables(),
|
||||
ragVariables: this.getRagVariables(),
|
||||
}
|
||||
}
|
||||
|
||||
getVariables(): string[] {
|
||||
const self = this.getLatest()
|
||||
return self.__variables
|
||||
}
|
||||
|
||||
getWorkflowNodesMap(): WorkflowNodesMap {
|
||||
const self = this.getLatest()
|
||||
return self.__workflowNodesMap
|
||||
}
|
||||
|
||||
getVarType(): any {
|
||||
const self = this.getLatest()
|
||||
return self.__getVarType
|
||||
}
|
||||
|
||||
getEnvironmentVariables(): any {
|
||||
const self = this.getLatest()
|
||||
return self.__environmentVariables
|
||||
}
|
||||
|
||||
getConversationVariables(): any {
|
||||
const self = this.getLatest()
|
||||
return self.__conversationVariables
|
||||
}
|
||||
|
||||
getRagVariables(): any {
|
||||
const self = this.getLatest()
|
||||
return self.__ragVariables
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return `{{#${this.getVariables().join('.')}#}}`
|
||||
}
|
||||
}
|
||||
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[]): WorkflowVariableBlockNode {
|
||||
return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, environmentVariables, conversationVariables, ragVariables)
|
||||
}
|
||||
|
||||
export function $isWorkflowVariableBlockNode(
|
||||
node: WorkflowVariableBlockNode | LexicalNode | null | undefined,
|
||||
): node is WorkflowVariableBlockNode {
|
||||
return node instanceof WorkflowVariableBlockNode
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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, resetReg } from '@/config'
|
||||
|
||||
const WorkflowVariableBlockReplacementBlock = ({
|
||||
workflowNodesMap,
|
||||
getVarType,
|
||||
onInsert,
|
||||
variables,
|
||||
}: WorkflowVariableBlockType) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const ragVariables = variables?.reduce<any[]>((acc, curr) => {
|
||||
if (curr.nodeId === 'rag')
|
||||
acc.push(...curr.vars)
|
||||
else
|
||||
acc.push(...curr.vars.filter(v => v.isRagVariable))
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
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, getVarType, variables?.find(o => o.nodeId === 'env')?.vars || [], variables?.find(o => o.nodeId === 'conversation')?.vars || [], ragVariables))
|
||||
}, [onInsert, workflowNodesMap, getVarType, variables])
|
||||
|
||||
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: CustomTextNode) => {
|
||||
resetReg()
|
||||
return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode)
|
||||
}, [createWorkflowVariableBlockNode, getMatch])
|
||||
|
||||
useEffect(() => {
|
||||
resetReg()
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, transformListener),
|
||||
)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(WorkflowVariableBlockReplacementBlock)
|
||||
Reference in New Issue
Block a user