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'
|
||||
Reference in New Issue
Block a user