prompt editor

This commit is contained in:
StyleZhang 2024-03-29 14:55:51 +08:00
parent 06a6d398cd
commit d7be9c0afc
26 changed files with 991 additions and 1070 deletions

View File

@ -216,10 +216,13 @@ const AdvancedPromptInput: FC<Props> = ({
onAddContext: showSelectDataSet,
}}
variableBlock={{
show: true,
variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({
name: item.name,
value: item.key,
})),
}}
externalToolBlock={{
externalTools: modelConfig.configs.prompt_variables.filter(item => item.type === 'api').map(item => ({
name: item.name,
variableName: item.key,

View File

@ -178,10 +178,14 @@ const Prompt: FC<ISimplePromptInput> = ({
onAddContext: showSelectDataSet,
}}
variableBlock={{
show: true,
variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({
name: item.name,
value: item.key,
})),
}}
externalToolBlock={{
show: true,
externalTools: modelConfig.configs.prompt_variables.filter(item => item.type === 'api').map(item => ({
name: item.name,
variableName: item.key,

View File

@ -102,10 +102,14 @@ const Editor: FC<Props> = ({
onAddContext: showSelectDataSet,
}}
variableBlock={{
show: true,
variables: modelConfig.configs.prompt_variables.map(item => ({
name: item.name,
value: item.key,
})),
}}
externalToolBlock={{
show: true,
externalTools: externalDataToolsConfig.map(item => ({
name: item.label!,
variableName: item.variable!,

View File

@ -16,19 +16,26 @@ import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
// import TreeView from './plugins/tree-view'
import Placeholder from './plugins/placeholder'
import ComponentPicker from './plugins/component-picker'
import VariablePicker from './plugins/variable-picker'
import ContextBlock from './plugins/context-block'
import { ContextBlockNode } from './plugins/context-block/node'
import ContextBlockReplacementBlock from './plugins/context-block-replacement-block'
import HistoryBlock from './plugins/history-block'
import { HistoryBlockNode } from './plugins/history-block/node'
import HistoryBlockReplacementBlock from './plugins/history-block-replacement-block'
import QueryBlock from './plugins/query-block'
import { QueryBlockNode } from './plugins/query-block/node'
import WorkflowVariableBlock from './plugins/workflow-variable-block'
import { WorkflowVariableBlockNode } from './plugins/workflow-variable-block/node'
import QueryBlockReplacementBlock from './plugins/query-block-replacement-block'
import ComponentPickerBlock from './plugins/component-picker-block'
import {
ContextBlock,
ContextBlockNode,
ContextBlockReplacementBlock,
} from './plugins/context-block'
import {
QueryBlock,
QueryBlockNode,
QueryBlockReplacementBlock,
} from './plugins/query-block'
import {
HistoryBlock,
HistoryBlockNode,
HistoryBlockReplacementBlock,
} from './plugins/history-block'
import {
WorkflowVariableBlock,
WorkflowVariableBlockNode,
} from './plugins/workflow-variable-block'
import VariableBlock from './plugins/variable-block'
import VariableValueBlock from './plugins/variable-value-block'
import { VariableValueBlockNode } from './plugins/variable-value-block/node'
@ -36,18 +43,19 @@ import { CustomTextNode } from './plugins/custom-text/node'
import OnBlurBlock from './plugins/on-blur-or-focus-block'
import UpdateBlock from './plugins/update-block'
import { textToEditorState } from './utils'
import type { Dataset } from './plugins/context-block'
import type { RoleName } from './plugins/history-block'
import type { ExternalToolOption, Option } from './plugins/variable-picker'
import type {
ContextBlockType,
ExternalToolBlockType,
HistoryBlockType,
QueryBlockType,
VariableBlockType,
WorkflowVariableBlockType,
} from './types'
import {
UPDATE_DATASETS_EVENT_EMITTER,
UPDATE_HISTORY_EVENT_EMITTER,
} from './constants'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
export type PromptEditorProps = {
className?: string
@ -55,47 +63,15 @@ export type PromptEditorProps = {
style?: React.CSSProperties
value?: string
editable?: boolean
outToolDisabled?: boolean
canNotAddContext?: boolean
onChange?: (text: string) => void
onBlur?: () => void
onFocus?: () => void
contextBlock?: {
show?: boolean
selectable?: boolean
datasets: Dataset[]
onInsert?: () => void
onDelete?: () => void
onAddContext: () => void
}
variableBlock?: {
selectable?: boolean
variables: Option[]
externalTools?: ExternalToolOption[]
onAddExternalTool?: () => void
}
historyBlock?: {
show?: boolean
selectable?: boolean
history: RoleName
onInsert?: () => void
onDelete?: () => void
onEditRole: () => void
}
queryBlock?: {
show?: boolean
selectable?: boolean
onInsert?: () => void
onDelete?: () => void
}
workflowVariableBlock?: {
show?: boolean
selectable?: boolean
variables: NodeOutPutVar[]
getWorkflowNode: (nodeId: string) => Node | undefined
onInsert?: () => void
onDelete?: () => void
}
contextBlock?: ContextBlockType
queryBlock?: QueryBlockType
historyBlock?: HistoryBlockType
variableBlock?: VariableBlockType
externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType
}
const PromptEditor: FC<PromptEditorProps> = ({
@ -104,47 +80,15 @@ const PromptEditor: FC<PromptEditorProps> = ({
style,
value,
editable = true,
outToolDisabled = false,
canNotAddContext = false,
onChange,
onBlur,
onFocus,
contextBlock = {
show: true,
selectable: true,
datasets: [],
onAddContext: () => { },
onInsert: () => { },
onDelete: () => { },
},
historyBlock = {
show: true,
selectable: true,
history: {
user: '',
assistant: '',
},
onEditRole: () => { },
onInsert: () => { },
onDelete: () => { },
},
variableBlock = {
variables: [],
},
queryBlock = {
show: true,
selectable: true,
onInsert: () => { },
onDelete: () => { },
},
workflowVariableBlock = {
show: true,
selectable: true,
variables: [],
getWorkflowNode: () => { },
onInsert: () => { },
onDelete: () => { },
},
contextBlock,
queryBlock,
historyBlock,
variableBlock,
externalToolBlock,
workflowVariableBlock,
}) => {
const { eventEmitter } = useEventEmitterContextContext()
const initialConfig = {
@ -176,15 +120,15 @@ const PromptEditor: FC<PromptEditorProps> = ({
useEffect(() => {
eventEmitter?.emit({
type: UPDATE_DATASETS_EVENT_EMITTER,
payload: contextBlock.datasets,
payload: contextBlock?.datasets,
} as any)
}, [eventEmitter, contextBlock.datasets])
}, [eventEmitter, contextBlock?.datasets])
useEffect(() => {
eventEmitter?.emit({
type: UPDATE_HISTORY_EVENT_EMITTER,
payload: historyBlock.history,
payload: historyBlock?.history,
} as any)
}, [eventEmitter, historyBlock.history])
}, [eventEmitter, historyBlock?.history])
return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
@ -194,83 +138,63 @@ const PromptEditor: FC<PromptEditorProps> = ({
placeholder={<Placeholder value={placeholder} />}
ErrorBoundary={LexicalErrorBoundary}
/>
<ComponentPicker
contextDisabled={!contextBlock.selectable}
contextShow={contextBlock.show}
historyDisabled={!historyBlock.selectable}
historyShow={historyBlock.show}
queryDisabled={!queryBlock.selectable}
queryShow={queryBlock.show}
outToolDisabled={outToolDisabled}
workflowVariableShow={workflowVariableBlock.show}
workflowVariables={workflowVariableBlock.variables}
<ComponentPickerBlock
triggerString='/'
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
/>
<VariablePicker
items={variableBlock.variables}
externalTools={variableBlock.externalTools}
onAddExternalTool={variableBlock.onAddExternalTool}
outToolDisabled={outToolDisabled}
<ComponentPickerBlock
triggerString='{'
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
/>
{
contextBlock.show && (
contextBlock?.show && (
<>
<ContextBlock
datasets={contextBlock.datasets}
onAddContext={contextBlock.onAddContext}
onInsert={contextBlock.onInsert}
onDelete={contextBlock.onDelete}
canNotAddContext={canNotAddContext}
/>
<ContextBlockReplacementBlock
datasets={contextBlock.datasets}
onAddContext={contextBlock.onAddContext}
onInsert={contextBlock.onInsert}
canNotAddContext={canNotAddContext}
/>
</>
)
}
<VariableBlock />
{
historyBlock.show && (
<>
<HistoryBlock
roleName={historyBlock.history}
onEditRole={historyBlock.onEditRole}
onInsert={historyBlock.onInsert}
onDelete={historyBlock.onDelete}
/>
<HistoryBlockReplacementBlock
roleName={historyBlock.history}
onEditRole={historyBlock.onEditRole}
onInsert={historyBlock.onInsert}
/>
<ContextBlock {...contextBlock} />
<ContextBlockReplacementBlock {...contextBlock} />
</>
)
}
{
queryBlock.show && (
queryBlock?.show && (
<>
<QueryBlock
onInsert={queryBlock.onInsert}
onDelete={queryBlock.onDelete}
/>
<QueryBlock {...queryBlock} />
<QueryBlockReplacementBlock />
</>
)
}
{
workflowVariableBlock.show && (
historyBlock?.show && (
<>
<WorkflowVariableBlock
getWorkflowNode={workflowVariableBlock.getWorkflowNode as any}
onInsert={workflowVariableBlock.onInsert}
onDelete={workflowVariableBlock.onDelete}
/>
<HistoryBlock {...historyBlock} />
<HistoryBlockReplacementBlock {...historyBlock} />
</>
)
}
{
(variableBlock?.show || externalToolBlock?.show) && (
<>
<VariableBlock />
<VariableValueBlock />
</>
)
}
{
workflowVariableBlock?.show && (
<>
<WorkflowVariableBlock {...workflowVariableBlock} />
</>
)
}
<VariableValueBlock />
<OnChangePlugin onChange={handleEditorChange} />
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
<UpdateBlock />

View File

@ -0,0 +1,85 @@
import { memo } from 'react'
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
export class VariableOption extends MenuOption {
title: string
icon?: JSX.Element
extraElement?: JSX.Element
keywords: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
constructor(
title: string,
options: {
icon?: JSX.Element
extraElement?: JSX.Element
keywords?: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
},
) {
super(title)
this.title = title
this.keywords = options.keywords || []
this.icon = options.icon
this.extraElement = options.extraElement
this.keyboardShortcut = options.keyboardShortcut
this.onSelect = options.onSelect.bind(this)
}
}
type VariableMenuItemProps = {
isSelected: boolean
onClick: () => void
onMouseEnter: () => void
option: VariableOption
queryString: string | null
}
export const VariableMenuItem = memo(({
isSelected,
onClick,
onMouseEnter,
option,
queryString,
}: VariableMenuItemProps) => {
const title = option.title
let before = title
let middle = ''
let after = ''
if (queryString) {
const regex = new RegExp(queryString, 'i')
const match = regex.exec(option.title)
if (match) {
before = title.substring(0, match.index)
middle = match[0]
after = title.substring(match.index + match[0].length)
}
}
return (
<div
key={option.key}
className={`
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
${isSelected && 'bg-primary-50'}
`}
tabIndex={-1}
ref={option.setRefElement}
onMouseEnter={onMouseEnter}
onClick={onClick}>
<div className='mr-2'>
{option.icon}
</div>
<div className='grow text-[13px] text-gray-900 truncate' title={option.title}>
{before}
<span className='text-[#2970FF]'>{middle}</span>
{after}
</div>
{option.extraElement}
</div>
)
})
VariableMenuItem.displayName = 'VariableMenuItem'

View File

@ -0,0 +1,192 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { $insertNodes } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type {
ContextBlockType,
ExternalToolBlockType,
HistoryBlockType,
QueryBlockType,
VariableBlockType,
} from '../../types'
import { INSERT_CONTEXT_BLOCK_COMMAND } from '../context-block'
import { INSERT_HISTORY_BLOCK_COMMAND } from '../history-block'
import { INSERT_QUERY_BLOCK_COMMAND } from '../query-block'
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
import { $createCustomTextNode } from '../custom-text/node'
import { PromptOption } from './prompt-option'
import { VariableOption } from './variable-option'
import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
import {
MessageClockCircle,
Tool03,
} from '@/app/components/base/icons/src/vender/solid/general'
import { BracketsX } from '@/app/components/base/icons/src/vender/line/development'
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
import AppIcon from '@/app/components/base/app-icon'
export const usePromptOptions = (
contextBlock?: ContextBlockType,
queryBlock?: QueryBlockType,
historyBlock?: HistoryBlockType,
) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
return useMemo(() => {
return [
...contextBlock?.show
? [
new PromptOption(t('common.promptEditor.context.item.title'), {
icon: <File05 className='w-4 h-4 text-[#6938EF]' />,
onSelect: () => {
if (contextBlock?.selectable)
return
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
},
disabled: contextBlock?.selectable,
}),
]
: [],
...queryBlock?.show
? [
new PromptOption(t('common.promptEditor.query.item.title'), {
icon: <UserEdit02 className='w-4 h-4 text-[#FD853A]' />,
onSelect: () => {
if (queryBlock?.selectable)
return
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
},
disabled: queryBlock?.selectable,
}),
]
: [],
...historyBlock?.show
? [
new PromptOption(t('common.promptEditor.history.item.title'), {
icon: <MessageClockCircle className='w-4 h-4 text-[#DD2590]' />,
onSelect: () => {
if (historyBlock?.selectable)
return
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
},
disabled: historyBlock?.selectable,
}),
]
: [],
]
}, [contextBlock, editor, historyBlock, queryBlock, t])
}
export const useVariableOptions = (
variableBlock?: VariableBlockType,
queryString?: string,
) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const options = useMemo(() => {
const baseOptions = (variableBlock?.variables || []).map((item) => {
return new VariableOption(item.value, {
icon: <BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />,
onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
},
})
})
if (!queryString)
return baseOptions
const regex = new RegExp(queryString, 'i')
return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
}, [editor, queryString, variableBlock])
const addOption = useMemo(() => {
return new VariableOption(t('common.promptEditor.variable.modal.add'), {
icon: <BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />,
onSelect: () => {
editor.update(() => {
const prefixNode = $createCustomTextNode('{{')
const suffixNode = $createCustomTextNode('}}')
$insertNodes([prefixNode, suffixNode])
prefixNode.select()
})
},
})
}, [editor, t])
return useMemo(() => {
return variableBlock?.show ? [...options, addOption] : []
}, [options, addOption, variableBlock?.show])
}
export const useExternalToolOptions = (
externalToolBlockType?: ExternalToolBlockType,
queryString?: string,
) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const options = useMemo(() => {
const baseToolOptions = (externalToolBlockType?.externalTools || []).map((item) => {
return new VariableOption(item.name, {
icon: (
<AppIcon
className='!w-[14px] !h-[14px]'
icon={item.icon}
background={item.icon_background}
/>
),
extraElement: <div className='text-xs text-gray-400'>{item.variableName}</div>,
onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
},
})
})
if (!queryString)
return baseToolOptions
const regex = new RegExp(queryString, 'i')
return baseToolOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
}, [editor, queryString, externalToolBlockType])
const addOption = useMemo(() => {
return new VariableOption(t('common.promptEditor.variable.modal.addTool'), {
icon: <Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />,
extraElement: <ArrowUpRight className='w-3 h-3 text-gray-400' />,
onSelect: () => {
if (externalToolBlockType?.onAddExternalTool)
externalToolBlockType.onAddExternalTool()
},
})
}, [externalToolBlockType, t])
return useMemo(() => {
return externalToolBlockType?.show ? [...options, addOption] : []
}, [options, addOption, externalToolBlockType?.show])
}
export const useOptions = (
contextBlock?: ContextBlockType,
queryBlock?: QueryBlockType,
historyBlock?: HistoryBlockType,
variableBlock?: VariableBlockType,
externalToolBlockType?: ExternalToolBlockType,
queryString?: string,
) => {
const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock)
const variableOptions = useVariableOptions(variableBlock, queryString)
const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString)
return useMemo(() => {
return {
promptOptions,
variableOptions,
externalToolOptions,
allOptions: [...promptOptions, ...variableOptions, ...externalToolOptions],
}
}, [promptOptions, variableOptions, externalToolOptions])
}

View File

@ -0,0 +1,226 @@
import {
memo,
useCallback,
useState,
} from 'react'
import ReactDOM from 'react-dom'
import type { TextNode } from 'lexical'
import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import type {
ContextBlockType,
ExternalToolBlockType,
HistoryBlockType,
QueryBlockType,
VariableBlockType,
WorkflowVariableBlockType,
} from '../../types'
import { useBasicTypeaheadTriggerMatch } from '../../hooks'
import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block'
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
import type { PromptOption } from './prompt-option'
import PromptMenu from './prompt-menu'
import VariableMenu from './variable-menu'
import type { VariableOption } from './variable-option'
import { useOptions } from './hooks'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { useEventEmitterContextContext } from '@/context/event-emitter'
type ComponentPickerProps = {
triggerString: string
contextBlock?: ContextBlockType
queryBlock?: QueryBlockType
historyBlock?: HistoryBlockType
variableBlock?: VariableBlockType
externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType
}
const ComponentPicker = ({
triggerString,
contextBlock,
queryBlock,
historyBlock,
variableBlock,
externalToolBlock,
workflowVariableBlock,
}: ComponentPickerProps) => {
const { eventEmitter } = useEventEmitterContextContext()
const [editor] = useLexicalComposerContext()
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, {
minLength: 0,
maxLength: 0,
})
const [queryString, setQueryString] = useState<string | null>(null)
eventEmitter?.useSubscription((v: any) => {
if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
})
const {
allOptions,
promptOptions,
variableOptions,
externalToolOptions,
} = useOptions(
contextBlock,
queryBlock,
historyBlock,
variableBlock,
externalToolBlock,
)
const onSelectOption = useCallback(
(
selectedOption: PromptOption | VariableOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
matchingString: string,
) => {
editor.update(() => {
if (nodeToRemove)
nodeToRemove.remove()
selectedOption.onSelect(matchingString)
closeMenu()
})
},
[editor],
)
const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
}, [editor])
const renderMenu = useCallback<MenuRenderFn<PromptOption | VariableOption>>((
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
) => {
if (anchorElementRef.current && allOptions.length) {
return ReactDOM.createPortal(
<div className='mt-[25px] w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
{
!!promptOptions.length && (
<>
<PromptMenu
startIndex={0}
selectedIndex={selectedIndex}
options={promptOptions}
onClick={(index, option) => {
if (option.disabled)
return
setHighlightedIndex(index)
selectOptionAndCleanUp(option)
}}
onMouseEnter={(index, option) => {
if (option.disabled)
return
setHighlightedIndex(index)
}}
/>
</>
)
}
{
!!variableOptions.length && (
<>
{
!!promptOptions.length && (
<div className='h-[1px] bg-gray-100'></div>
)
}
<VariableMenu
startIndex={promptOptions.length}
selectedIndex={selectedIndex}
options={variableOptions}
onClick={(index, option) => {
if (option.disabled)
return
setHighlightedIndex(index)
selectOptionAndCleanUp(option)
}}
onMouseEnter={(index, option) => {
if (option.disabled)
return
setHighlightedIndex(index)
}}
queryString={queryString}
/>
</>
)
}
{
!!externalToolOptions.length && (
<>
{
(!!promptOptions.length || !!variableOptions.length) && (
<div className='h-[1px] bg-gray-100'></div>
)
}
<VariableMenu
startIndex={promptOptions.length + variableOptions.length}
selectedIndex={selectedIndex}
options={externalToolOptions}
onClick={(index, option) => {
if (option.disabled)
return
setHighlightedIndex(index)
selectOptionAndCleanUp(option)
}}
onMouseEnter={(index, option) => {
if (option.disabled)
return
setHighlightedIndex(index)
}}
queryString={queryString}
/>
</>
)
}
{
workflowVariableBlock?.show && !!workflowVariableBlock?.variables?.length && (
<>
{
(!!promptOptions.length || !!variableOptions.length || !!externalToolOptions.length) && (
<div className='h-[1px] bg-gray-100'></div>
)
}
<VarReferenceVars
hideSearch
vars={workflowVariableBlock?.variables}
onChange={handleSelectWorkflowVariable}
/>
</>
)
}
</div>,
anchorElementRef.current,
)
}
return null
}, [
allOptions,
promptOptions,
variableOptions,
externalToolOptions,
queryString,
workflowVariableBlock,
handleSelectWorkflowVariable,
])
return (
<LexicalTypeaheadMenuPlugin
options={allOptions}
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
anchorClassName='z-[999999]'
menuRenderFn={renderMenu}
triggerFn={checkForTriggerMatch}
/>
)
}
export default memo(ComponentPicker)

View File

@ -0,0 +1,37 @@
import { memo } from 'react'
import { PromptMenuItem } from './prompt-option'
type PromptMenuProps = {
startIndex: number
selectedIndex: number | null
options: any[]
onClick: (index: number, option: any) => void
onMouseEnter: (index: number, option: any) => void
}
const PromptMenu = ({
startIndex,
selectedIndex,
options,
onClick,
onMouseEnter,
}: PromptMenuProps) => {
return (
<div className='p-1'>
{
options.map((option, index: number) => (
<PromptMenuItem
startIndex={startIndex}
index={index}
isSelected={selectedIndex === index + startIndex}
onClick={onClick}
onMouseEnter={onMouseEnter}
key={option.key}
option={option}
/>
))
}
</div>
)
}
export default memo(PromptMenu)

View File

@ -0,0 +1,65 @@
import { memo } from 'react'
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
export class PromptOption extends MenuOption {
title: string
icon?: JSX.Element
keywords: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
disabled?: boolean
constructor(
title: string,
options: {
icon?: JSX.Element
keywords?: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
disabled?: boolean
},
) {
super(title)
this.title = title
this.keywords = options.keywords || []
this.icon = options.icon
this.keyboardShortcut = options.keyboardShortcut
this.onSelect = options.onSelect.bind(this)
this.disabled = options.disabled
}
}
type PromptMenuItemMenuItemProps = {
startIndex: number
index: number
isSelected: boolean
onClick: (index: number, option: PromptOption) => void
onMouseEnter: (index: number, option: PromptOption) => void
option: PromptOption
}
export const PromptMenuItem = memo(({
startIndex,
index,
isSelected,
onClick,
onMouseEnter,
option,
}: PromptMenuItemMenuItemProps) => {
return (
<div
key={option.key}
className={`
flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50
${isSelected && !option.disabled && '!bg-gray-50'}
${option.disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
`}
tabIndex={-1}
ref={option.setRefElement}
onMouseEnter={() => onMouseEnter(index + startIndex, option)}
onClick={() => onClick(index + startIndex, option)}>
{option.icon}
<div className='ml-1 text-[13px] text-gray-900'>{option.title}</div>
</div>
)
})
PromptMenuItem.displayName = 'PromptMenuItem'

View File

@ -0,0 +1,40 @@
import { memo } from 'react'
import { VariableMenuItem } from './variable-option'
type VariableMenuProps = {
startIndex: number
selectedIndex: number | null
options: any[]
onClick: (index: number, option: any) => void
onMouseEnter: (index: number, option: any) => void
queryString: string | null
}
const VariableMenu = ({
startIndex,
selectedIndex,
options,
onClick,
onMouseEnter,
queryString,
}: VariableMenuProps) => {
return (
<div className='p-1'>
{
options.map((option, index: number) => (
<VariableMenuItem
startIndex={startIndex}
index={index}
isSelected={selectedIndex === index + startIndex}
onClick={onClick}
onMouseEnter={onMouseEnter}
key={option.key}
option={option}
queryString={queryString}
/>
))
}
</div>
)
}
export default memo(VariableMenu)

View File

@ -0,0 +1,89 @@
import { memo } from 'react'
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
export class VariableOption extends MenuOption {
title: string
icon?: JSX.Element
extraElement?: JSX.Element
keywords: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
constructor(
title: string,
options: {
icon?: JSX.Element
extraElement?: JSX.Element
keywords?: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
},
) {
super(title)
this.title = title
this.keywords = options.keywords || []
this.icon = options.icon
this.extraElement = options.extraElement
this.keyboardShortcut = options.keyboardShortcut
this.onSelect = options.onSelect.bind(this)
}
}
type VariableMenuItemProps = {
startIndex: number
index: number
isSelected: boolean
onClick: (index: number, option: VariableOption) => void
onMouseEnter: (index: number, option: VariableOption) => void
option: VariableOption
queryString: string | null
}
export const VariableMenuItem = memo(({
startIndex,
index,
isSelected,
onClick,
onMouseEnter,
option,
queryString,
}: VariableMenuItemProps) => {
const title = option.title
let before = title
let middle = ''
let after = ''
if (queryString) {
const regex = new RegExp(queryString, 'i')
const match = regex.exec(option.title)
if (match) {
before = title.substring(0, match.index)
middle = match[0]
after = title.substring(match.index + match[0].length)
}
}
return (
<div
key={option.key}
className={`
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
${isSelected && 'bg-primary-50'}
`}
tabIndex={-1}
ref={option.setRefElement}
onMouseEnter={() => onMouseEnter(index + startIndex, option)}
onClick={() => onClick(index + startIndex, option)}>
<div className='mr-2'>
{option.icon}
</div>
<div className='grow text-[13px] text-gray-900 truncate' title={option.title}>
{before}
<span className='text-[#2970FF]'>{middle}</span>
{after}
</div>
{option.extraElement}
</div>
)
})
VariableMenuItem.displayName = 'VariableMenuItem'

View File

@ -1,247 +0,0 @@
import type { FC } from 'react'
import { useCallback } from 'react'
import ReactDOM from 'react-dom'
import { useTranslation } from 'react-i18next'
import type { TextNode } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
LexicalTypeaheadMenuPlugin,
MenuOption,
} from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { useBasicTypeaheadTriggerMatch } from '../hooks'
import { INSERT_CONTEXT_BLOCK_COMMAND } from './context-block'
import { INSERT_VARIABLE_BLOCK_COMMAND } from './variable-block'
import { INSERT_HISTORY_BLOCK_COMMAND } from './history-block'
import { INSERT_QUERY_BLOCK_COMMAND } from './query-block'
import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from './workflow-variable-block'
import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
import { Variable } from '@/app/components/base/icons/src/vender/line/development'
import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
class ComponentPickerOption extends MenuOption {
title: string
icon?: JSX.Element
keywords: Array<string>
keyboardShortcut?: string
desc: string
onSelect: (queryString: string) => void
disabled?: boolean
constructor(
title: string,
options: {
icon?: JSX.Element
keywords?: Array<string>
keyboardShortcut?: string
desc: string
onSelect: (queryString: string) => void
disabled?: boolean
},
) {
super(title)
this.title = title
this.keywords = options.keywords || []
this.icon = options.icon
this.keyboardShortcut = options.keyboardShortcut
this.desc = options.desc
this.onSelect = options.onSelect.bind(this)
this.disabled = options.disabled
}
}
type ComponentPickerMenuItemProps = {
isSelected: boolean
onClick: () => void
onMouseEnter: () => void
option: ComponentPickerOption
}
const ComponentPickerMenuItem: FC<ComponentPickerMenuItemProps> = ({
isSelected,
onClick,
onMouseEnter,
option,
}) => {
const { t } = useTranslation()
return (
<div
key={option.key}
className={`
flex items-center px-3 py-1.5 rounded-lg
${isSelected && !option.disabled && '!bg-gray-50'}
${option.disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
`}
tabIndex={-1}
ref={option.setRefElement}
onMouseEnter={onMouseEnter}
onClick={onClick}>
<div className='flex items-center justify-center mr-2 w-8 h-8 rounded-lg border border-gray-100'>
{option.icon}
</div>
<div className='grow'>
<div className='flex items-center justify-between h-5 text-sm text-gray-900'>
{option.title}
<span className='text-xs text-gray-400'>{option.disabled && t('common.promptEditor.existed')}</span>
</div>
<div className='text-xs text-gray-500'>{option.desc}</div>
</div>
</div>
)
}
type ComponentPickerProps = {
contextDisabled?: boolean
historyDisabled?: boolean
queryDisabled?: boolean
contextShow?: boolean
historyShow?: boolean
queryShow?: boolean
outToolDisabled?: boolean
workflowVariableShow?: boolean
workflowVariables?: NodeOutPutVar[]
}
const ComponentPicker: FC<ComponentPickerProps> = ({
contextDisabled,
historyDisabled,
queryDisabled,
contextShow,
historyShow,
queryShow,
outToolDisabled,
workflowVariableShow,
workflowVariables,
}) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
minLength: 0,
maxLength: 0,
})
const options = [
...contextShow
? [
new ComponentPickerOption(t('common.promptEditor.context.item.title'), {
desc: t('common.promptEditor.context.item.desc'),
icon: <File05 className='w-4 h-4 text-[#6938EF]' />,
onSelect: () => {
if (contextDisabled)
return
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
},
disabled: contextDisabled,
}),
]
: [],
new ComponentPickerOption(t(`common.promptEditor.variable.${!outToolDisabled ? 'item' : 'outputToolDisabledItem'}.title`), {
desc: t(`common.promptEditor.variable.${!outToolDisabled ? 'item' : 'outputToolDisabledItem'}.desc`),
icon: <Variable className='w-4 h-4 text-[#2970FF]' />,
onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_BLOCK_COMMAND, undefined)
},
}),
...historyShow
? [
new ComponentPickerOption(t('common.promptEditor.history.item.title'), {
desc: t('common.promptEditor.history.item.desc'),
icon: <MessageClockCircle className='w-4 h-4 text-[#DD2590]' />,
onSelect: () => {
if (historyDisabled)
return
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
},
disabled: historyDisabled,
}),
]
: [],
...queryShow
? [
new ComponentPickerOption(t('common.promptEditor.query.item.title'), {
desc: t('common.promptEditor.query.item.desc'),
icon: <UserEdit02 className='w-4 h-4 text-[#FD853A]' />,
onSelect: () => {
if (queryDisabled)
return
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
},
disabled: queryDisabled,
}),
]
: [],
]
const onSelectOption = useCallback(
(
selectedOption: ComponentPickerOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
matchingString: string,
) => {
editor.update(() => {
if (nodeToRemove)
nodeToRemove.remove()
selectedOption.onSelect(matchingString)
closeMenu()
})
},
[editor],
)
const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
}, [editor])
return (
<LexicalTypeaheadMenuPlugin
options={options}
onQueryChange={() => { }}
onSelectOption={onSelectOption}
anchorClassName='z-[999999]'
menuRenderFn={(
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
) =>
(anchorElementRef.current && options.length)
? ReactDOM.createPortal(
<div className='mt-[25px] p-1 w-[400px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
{options.map((option, i: number) => (
<ComponentPickerMenuItem
isSelected={selectedIndex === i}
onClick={() => {
if (option.disabled)
return
setHighlightedIndex(i)
selectOptionAndCleanUp(option)
}}
onMouseEnter={() => {
if (option.disabled)
return
setHighlightedIndex(i)
}}
key={option.key}
option={option}
/>
))}
{
workflowVariableShow && !!workflowVariables?.length && (
<VarReferenceVars
hideSearch
vars={workflowVariables}
onChange={handleSelectWorkflowVariable}
/>
)
}
</div>,
anchorElementRef.current,
)
: null}
triggerFn={checkForTriggerMatch}
/>
)
}
export default ComponentPicker

View File

@ -1,28 +1,28 @@
import type { FC } from 'react'
import {
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../utils'
import { CONTEXT_PLACEHOLDER_TEXT } from '../constants'
import { decoratorTransform } from '../../utils'
import { CONTEXT_PLACEHOLDER_TEXT } from '../../constants'
import type { ContextBlockType } from '../../types'
import {
$createContextBlockNode,
ContextBlockNode,
} from './context-block/node'
import type { ContextBlockProps } from './context-block/index'
import { CustomTextNode } from './custom-text/node'
} from '../context-block/node'
import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT)
const ContextBlockReplacementBlock: FC<ContextBlockProps> = ({
datasets,
onAddContext,
const ContextBlockReplacementBlock = ({
datasets = [],
onAddContext = () => {},
onInsert,
canNotAddContext,
}) => {
}: ContextBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@ -34,7 +34,7 @@ const ContextBlockReplacementBlock: FC<ContextBlockProps> = ({
if (onInsert)
onInsert()
return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext, canNotAddContext))
}, [datasets, onAddContext, onInsert])
}, [datasets, onAddContext, onInsert, canNotAddContext])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
@ -54,9 +54,9 @@ const ContextBlockReplacementBlock: FC<ContextBlockProps> = ({
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)),
)
}, [])
}, [editor, getMatch, createContextBlockNode])
return null
}
export default ContextBlockReplacementBlock
export default memo(ContextBlockReplacementBlock)

View File

@ -1,5 +1,7 @@
import type { FC } from 'react'
import { useEffect } from 'react'
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
@ -7,6 +9,7 @@ import {
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { ContextBlockType } from '../../types'
import {
$createContextBlockNode,
ContextBlockNode,
@ -21,20 +24,13 @@ export type Dataset = {
type: string
}
export type ContextBlockProps = {
datasets: Dataset[]
onAddContext: () => void
onInsert?: () => void
onDelete?: () => void
canNotAddContext?: boolean
}
const ContextBlock: FC<ContextBlockProps> = ({
datasets,
onAddContext,
const ContextBlock = memo(({
datasets = [],
onAddContext = () => {},
onInsert,
onDelete,
canNotAddContext,
}) => {
}: ContextBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@ -67,9 +63,12 @@ const ContextBlock: FC<ContextBlockProps> = ({
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, datasets, onAddContext, onInsert, onDelete])
}, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext])
return null
}
})
ContextBlock.displayName = 'ContextBlock'
export default ContextBlock
export { ContextBlock }
export { ContextBlockNode } from './node'
export { default as ContextBlockReplacementBlock } from './context-block-replacement-block'

View File

@ -1,4 +1,3 @@
import type { FC } from 'react'
import {
useCallback,
useEffect,
@ -6,22 +5,22 @@ import {
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../utils'
import { HISTORY_PLACEHOLDER_TEXT } from '../constants'
import { decoratorTransform } from '../../utils'
import { HISTORY_PLACEHOLDER_TEXT } from '../../constants'
import type { HistoryBlockType } from '../../types'
import {
$createHistoryBlockNode,
HistoryBlockNode,
} from './history-block/node'
import type { HistoryBlockProps } from './history-block/index'
import { CustomTextNode } from './custom-text/node'
} from '../history-block/node'
import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
const HistoryBlockReplacementBlock: FC<HistoryBlockProps> = ({
roleName,
onEditRole,
const HistoryBlockReplacementBlock = ({
history = { user: '', assistant: '' },
onEditRole = () => {},
onInsert,
}) => {
}: HistoryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@ -32,8 +31,8 @@ const HistoryBlockReplacementBlock: FC<HistoryBlockProps> = ({
const createHistoryBlockNode = useCallback((): HistoryBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createHistoryBlockNode(roleName, onEditRole))
}, [roleName, onEditRole, onInsert])
return $applyNodeReplacement($createHistoryBlockNode(history, onEditRole))
}, [history, onEditRole, onInsert])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)
@ -53,7 +52,7 @@ const HistoryBlockReplacementBlock: FC<HistoryBlockProps> = ({
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)),
)
}, [])
}, [editor, getMatch, createHistoryBlockNode])
return null
}

View File

@ -1,5 +1,7 @@
import type { FC } from 'react'
import { useEffect } from 'react'
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
@ -7,6 +9,7 @@ import {
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { HistoryBlockType } from '../../types'
import {
$createHistoryBlockNode,
HistoryBlockNode,
@ -27,12 +30,12 @@ export type HistoryBlockProps = {
onDelete?: () => void
}
const HistoryBlock: FC<HistoryBlockProps> = ({
roleName,
onEditRole,
const HistoryBlock = memo(({
history = { user: '', assistant: '' },
onEditRole = () => {},
onInsert,
onDelete,
}) => {
}: HistoryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@ -43,7 +46,7 @@ const HistoryBlock: FC<HistoryBlockProps> = ({
editor.registerCommand(
INSERT_HISTORY_BLOCK_COMMAND,
() => {
const historyBlockNode = $createHistoryBlockNode(roleName, onEditRole)
const historyBlockNode = $createHistoryBlockNode(history, onEditRole)
$insertNodes([historyBlockNode])
@ -65,9 +68,12 @@ const HistoryBlock: FC<HistoryBlockProps> = ({
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, roleName, onEditRole, onInsert, onDelete])
}, [editor, history, onEditRole, onInsert, onDelete])
return null
}
})
HistoryBlock.displayName = 'HistoryBlock'
export default HistoryBlock
export { HistoryBlock }
export { HistoryBlockNode } from './node'
export { default as HistoryBlockReplacementBlock } from './history-block-replacement-block'

View File

@ -40,7 +40,7 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onBlur])
}, [editor, onBlur, onFocus])
return null
}

View File

@ -1,5 +1,7 @@
import type { FC } from 'react'
import { useEffect } from 'react'
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
@ -7,6 +9,7 @@ import {
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { QueryBlockType } from '../../types'
import {
$createQueryBlockNode,
QueryBlockNode,
@ -19,10 +22,10 @@ export type QueryBlockProps = {
onInsert?: () => void
onDelete?: () => void
}
const QueryBlock: FC<QueryBlockProps> = ({
const QueryBlock = memo(({
onInsert,
onDelete,
}) => {
}: QueryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@ -57,6 +60,9 @@ const QueryBlock: FC<QueryBlockProps> = ({
}, [editor, onInsert, onDelete])
return null
}
})
QueryBlock.displayName = 'QueryBlock'
export default QueryBlock
export { QueryBlock }
export { QueryBlockNode } from './node'
export { default as QueryBlockReplacementBlock } from './query-block-replacement-block'

View File

@ -1,25 +1,25 @@
import type { FC } from 'react'
import {
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../utils'
import { QUERY_PLACEHOLDER_TEXT } from '../constants'
import { decoratorTransform } from '../../utils'
import { QUERY_PLACEHOLDER_TEXT } from '../../constants'
import type { QueryBlockType } from '../../types'
import {
$createQueryBlockNode,
QueryBlockNode,
} from './query-block/node'
import type { QueryBlockProps } from './query-block/index'
import { CustomTextNode } from './custom-text/node'
} from '../query-block/node'
import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT)
const QueryBlockReplacementBlock: FC<QueryBlockProps> = ({
const QueryBlockReplacementBlock = ({
onInsert,
}) => {
}: QueryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@ -51,9 +51,9 @@ const QueryBlockReplacementBlock: FC<QueryBlockProps> = ({
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)),
)
}, [])
}, [editor, getMatch, createQueryBlockNode])
return null
}
export default QueryBlockReplacementBlock
export default memo(QueryBlockReplacementBlock)

View File

@ -1,338 +0,0 @@
import type { FC } from 'react'
import { useCallback, useMemo, useState } from 'react'
import ReactDOM from 'react-dom'
import { useTranslation } from 'react-i18next'
import { $insertNodes, type TextNode } from 'lexical'
import {
LexicalTypeaheadMenuPlugin,
MenuOption,
} from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useBasicTypeaheadTriggerMatch } from '../hooks'
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from './variable-block'
import { $createCustomTextNode } from './custom-text/node'
import { BracketsX } from '@/app/components/base/icons/src/vender/line/development'
import { Tool03 } from '@/app/components/base/icons/src/vender/solid/general'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
import AppIcon from '@/app/components/base/app-icon'
import { useEventEmitterContextContext } from '@/context/event-emitter'
class VariablePickerOption extends MenuOption {
title: string
icon?: JSX.Element
extraElement?: JSX.Element
keywords: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
constructor(
title: string,
options: {
icon?: JSX.Element
extraElement?: JSX.Element
keywords?: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
},
) {
super(title)
this.title = title
this.keywords = options.keywords || []
this.icon = options.icon
this.extraElement = options.extraElement
this.keyboardShortcut = options.keyboardShortcut
this.onSelect = options.onSelect.bind(this)
}
}
type VariablePickerMenuItemProps = {
isSelected: boolean
onClick: () => void
onMouseEnter: () => void
option: VariablePickerOption
queryString: string | null
}
const VariablePickerMenuItem: FC<VariablePickerMenuItemProps> = ({
isSelected,
onClick,
onMouseEnter,
option,
queryString,
}) => {
const title = option.title
let before = title
let middle = ''
let after = ''
if (queryString) {
const regex = new RegExp(queryString, 'i')
const match = regex.exec(option.title)
if (match) {
before = title.substring(0, match.index)
middle = match[0]
after = title.substring(match.index + match[0].length)
}
}
return (
<div
key={option.key}
className={`
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
${isSelected && 'bg-primary-50'}
`}
tabIndex={-1}
ref={option.setRefElement}
onMouseEnter={onMouseEnter}
onClick={onClick}>
<div className='mr-2'>
{option.icon}
</div>
<div className='grow text-[13px] text-gray-900 truncate' title={option.title}>
{before}
<span className='text-[#2970FF]'>{middle}</span>
{after}
</div>
{option.extraElement}
</div>
)
}
export type Option = {
value: string
name: string
}
export type ExternalToolOption = {
name: string
variableName: string
icon?: string
icon_background?: string
}
type VariablePickerProps = {
items?: Option[]
externalTools?: ExternalToolOption[]
onAddExternalTool?: () => void
outToolDisabled?: boolean
}
const VariablePicker: FC<VariablePickerProps> = ({
items = [],
externalTools = [],
onAddExternalTool,
outToolDisabled,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
const [editor] = useLexicalComposerContext()
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('{', {
minLength: 0,
maxLength: 6,
})
const [queryString, setQueryString] = useState<string | null>(null)
eventEmitter?.useSubscription((v: any) => {
if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
})
const options = useMemo(() => {
const baseOptions = items.map((item) => {
return new VariablePickerOption(item.value, {
icon: <BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />,
onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
},
})
})
if (!queryString)
return baseOptions
const regex = new RegExp(queryString, 'i')
return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
}, [editor, queryString, items])
const toolOptions = useMemo(() => {
const baseToolOptions = externalTools.map((item) => {
return new VariablePickerOption(item.name, {
icon: (
<AppIcon
className='!w-[14px] !h-[14px]'
icon={item.icon}
background={item.icon_background}
/>
),
extraElement: <div className='text-xs text-gray-400'>{item.variableName}</div>,
onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
},
})
})
if (!queryString)
return baseToolOptions
const regex = new RegExp(queryString, 'i')
return baseToolOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
}, [editor, queryString, externalTools])
const newOption = new VariablePickerOption(t('common.promptEditor.variable.modal.add'), {
icon: <BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />,
onSelect: () => {
editor.update(() => {
const prefixNode = $createCustomTextNode('{{')
const suffixNode = $createCustomTextNode('}}')
$insertNodes([prefixNode, suffixNode])
prefixNode.select()
})
},
})
const newToolOption = new VariablePickerOption(t('common.promptEditor.variable.modal.addTool'), {
icon: <Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />,
extraElement: <ArrowUpRight className='w-3 h-3 text-gray-400' />,
onSelect: () => {
if (onAddExternalTool)
onAddExternalTool()
},
})
const onSelectOption = useCallback(
(
selectedOption: VariablePickerOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
matchingString: string,
) => {
editor.update(() => {
if (nodeToRemove)
nodeToRemove.remove()
selectedOption.onSelect(matchingString)
closeMenu()
})
},
[editor],
)
const mergedOptions = [...options, ...toolOptions, newOption]
if (!outToolDisabled)
mergedOptions.push(newToolOption)
return (
<LexicalTypeaheadMenuPlugin
options={mergedOptions}
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
anchorClassName='z-[999999]'
menuRenderFn={(
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
) =>
(anchorElementRef.current && mergedOptions.length)
? ReactDOM.createPortal(
<div className='mt-[25px] w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
{
!!options.length && (
<>
<div className='p-1'>
{options.map((option, i: number) => (
<VariablePickerMenuItem
isSelected={selectedIndex === i}
onClick={() => {
setHighlightedIndex(i)
selectOptionAndCleanUp(option)
}}
onMouseEnter={() => {
setHighlightedIndex(i)
}}
key={option.key}
option={option}
queryString={queryString}
/>
))}
</div>
<div className='h-[1px] bg-gray-100' />
</>
)
}
{
!!toolOptions.length && (
<>
<div className='p-1'>
{toolOptions.map((option, i: number) => (
<VariablePickerMenuItem
isSelected={selectedIndex === i + options.length}
onClick={() => {
setHighlightedIndex(i + options.length)
selectOptionAndCleanUp(option)
}}
onMouseEnter={() => {
setHighlightedIndex(i + options.length)
}}
key={option.key}
option={option}
queryString={queryString}
/>
))}
</div>
<div className='h-[1px] bg-gray-100' />
</>
)
}
<div className='p-1'>
<div
className={`
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
${selectedIndex === options.length + toolOptions.length && 'bg-primary-50'}
`}
ref={newOption.setRefElement}
tabIndex={-1}
onClick={() => {
setHighlightedIndex(options.length + toolOptions.length)
selectOptionAndCleanUp(newOption)
}}
onMouseEnter={() => {
setHighlightedIndex(options.length + toolOptions.length)
}}
key={newOption.key}
>
{newOption.icon}
<div className='text-[13px] text-gray-900'>{newOption.title}</div>
</div>
{!outToolDisabled && (
<div
className={`
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
${selectedIndex === options.length + toolOptions.length + 1 && 'bg-primary-50'}
`}
ref={newToolOption.setRefElement}
tabIndex={-1}
onClick={() => {
setHighlightedIndex(options.length + toolOptions.length + 1)
selectOptionAndCleanUp(newToolOption)
}}
onMouseEnter={() => {
setHighlightedIndex(options.length + toolOptions.length + 1)
}}
key={newToolOption.key}
>
{newToolOption.icon}
<div className='grow text-[13px] text-gray-900'>{newToolOption.title}</div>
{newToolOption.extraElement}
</div>
)}
</div>
</div>,
anchorElementRef.current,
)
: null}
triggerFn={checkForTriggerMatch}
/>
)
}
export default VariablePicker

View File

@ -1,236 +1,5 @@
function getHashtagRegexStringChars(): Readonly<{
alpha: string
alphanumeric: string
leftChars: string
rightChars: string
}> {
// Latin accented characters
// Excludes 0xd7 from the range
// (the multiplication sign, confusable with "x").
// Also excludes 0xf7, the division sign
const latinAccents
= '\xC0-\xD6'
+ '\xD8-\xF6'
+ '\xF8-\xFF'
+ '\u0100-\u024F'
+ '\u0253-\u0254'
+ '\u0256-\u0257'
+ '\u0259'
+ '\u025B'
+ '\u0263'
+ '\u0268'
+ '\u026F'
+ '\u0272'
+ '\u0289'
+ '\u028B'
+ '\u02BB'
+ '\u0300-\u036F'
+ '\u1E00-\u1EFF'
// Cyrillic (Russian, Ukrainian, etc.)
const nonLatinChars
= '\u0400-\u04FF' // Cyrillic
+ '\u0500-\u0527' // Cyrillic Supplement
+ '\u2DE0-\u2DFF' // Cyrillic Extended A
+ '\uA640-\uA69F' // Cyrillic Extended B
+ '\u0591-\u05BF' // Hebrew
+ '\u05C1-\u05C2'
+ '\u05C4-\u05C5'
+ '\u05C7'
+ '\u05D0-\u05EA'
+ '\u05F0-\u05F4'
+ '\uFB12-\uFB28' // Hebrew Presentation Forms
+ '\uFB2A-\uFB36'
+ '\uFB38-\uFB3C'
+ '\uFB3E'
+ '\uFB40-\uFB41'
+ '\uFB43-\uFB44'
+ '\uFB46-\uFB4F'
+ '\u0610-\u061A' // Arabic
+ '\u0620-\u065F'
+ '\u066E-\u06D3'
+ '\u06D5-\u06DC'
+ '\u06DE-\u06E8'
+ '\u06EA-\u06EF'
+ '\u06FA-\u06FC'
+ '\u06FF'
+ '\u0750-\u077F' // Arabic Supplement
+ '\u08A0' // Arabic Extended A
+ '\u08A2-\u08AC'
+ '\u08E4-\u08FE'
+ '\uFB50-\uFBB1' // Arabic Pres. Forms A
+ '\uFBD3-\uFD3D'
+ '\uFD50-\uFD8F'
+ '\uFD92-\uFDC7'
+ '\uFDF0-\uFDFB'
+ '\uFE70-\uFE74' // Arabic Pres. Forms B
+ '\uFE76-\uFEFC'
+ '\u200C-\u200C' // Zero-Width Non-Joiner
+ '\u0E01-\u0E3A' // Thai
+ '\u0E40-\u0E4E' // Hangul (Korean)
+ '\u1100-\u11FF' // Hangul Jamo
+ '\u3130-\u3185' // Hangul Compatibility Jamo
+ '\uA960-\uA97F' // Hangul Jamo Extended-A
+ '\uAC00-\uD7AF' // Hangul Syllables
+ '\uD7B0-\uD7FF' // Hangul Jamo Extended-B
+ '\uFFA1-\uFFDC' // Half-width Hangul
const charCode = String.fromCharCode
const cjkChars
= '\u30A1-\u30FA\u30FC-\u30FE' // Katakana (full-width)
+ '\uFF66-\uFF9F' // Katakana (half-width)
+ '\uFF10-\uFF19\uFF21-\uFF3A'
+ '\uFF41-\uFF5A' // Latin (full-width)
+ '\u3041-\u3096\u3099-\u309E' // Hiragana
+ '\u3400-\u4DBF' // Kanji (CJK Extension A)
+ `\u4E00-\u9FFF${// Kanji (Unified)
// Disabled as it breaks the Regex.
// charCode(0x20000) + '-' + charCode(0x2A6DF) + // Kanji (CJK Extension B)
charCode(0x2A700)
}-${
charCode(0x2B73F) // Kanji (CJK Extension C)
}${charCode(0x2B740)
}-${
charCode(0x2B81F) // Kanji (CJK Extension D)
}${charCode(0x2F800)
}-${
charCode(0x2FA1F)
}\u3003\u3005\u303B` // Kanji (CJK supplement)
const otherChars = latinAccents + nonLatinChars + cjkChars
// equivalent of \p{L}
const unicodeLetters
= '\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6'
+ '\u00F8-\u0241\u0250-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EE\u037A\u0386'
+ '\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03F5\u03F7-\u0481'
+ '\u048A-\u04CE\u04D0-\u04F9\u0500-\u050F\u0531-\u0556\u0559\u0561-\u0587'
+ '\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0640-\u064A\u066E-\u066F'
+ '\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710'
+ '\u0712-\u072F\u074D-\u076D\u0780-\u07A5\u07B1\u0904-\u0939\u093D\u0950'
+ '\u0958-\u0961\u097D\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0'
+ '\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1'
+ '\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33'
+ '\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D'
+ '\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD'
+ '\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30'
+ '\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83'
+ '\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F'
+ '\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10'
+ '\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C'
+ '\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE'
+ '\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39'
+ '\u0D60-\u0D61\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6'
+ '\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88'
+ '\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7'
+ '\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6'
+ '\u0EDC-\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6A\u0F88-\u0F8B\u1000-\u1021'
+ '\u1023-\u1027\u1029-\u102A\u1050-\u1055\u10A0-\u10C5\u10D0-\u10FA\u10FC'
+ '\u1100-\u1159\u115F-\u11A2\u11A8-\u11F9\u1200-\u1248\u124A-\u124D'
+ '\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0'
+ '\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310'
+ '\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C'
+ '\u166F-\u1676\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711'
+ '\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7'
+ '\u17DC\u1820-\u1877\u1880-\u18A8\u1900-\u191C\u1950-\u196D\u1970-\u1974'
+ '\u1980-\u19A9\u19C1-\u19C7\u1A00-\u1A16\u1D00-\u1DBF\u1E00-\u1E9B'
+ '\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D'
+ '\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC'
+ '\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC'
+ '\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107'
+ '\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D'
+ '\u212F-\u2131\u2133-\u2139\u213C-\u213F\u2145-\u2149\u2C00-\u2C2E'
+ '\u2C30-\u2C5E\u2C80-\u2CE4\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96'
+ '\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6'
+ '\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3006\u3031-\u3035'
+ '\u303B-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF'
+ '\u3105-\u312C\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5'
+ '\u4E00-\u9FBB\uA000-\uA48C\uA800-\uA801\uA803-\uA805\uA807-\uA80A'
+ '\uA80C-\uA822\uAC00-\uD7A3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9'
+ '\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C'
+ '\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F'
+ '\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A'
+ '\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7'
+ '\uFFDA-\uFFDC'
// equivalent of \p{Mn}\p{Mc}
const unicodeAccents
= '\u0300-\u036F\u0483-\u0486\u0591-\u05B9\u05BB-\u05BD\u05BF'
+ '\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u0615\u064B-\u065E\u0670'
+ '\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A'
+ '\u07A6-\u07B0\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u0962-\u0963'
+ '\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7'
+ '\u09E2-\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D'
+ '\u0A70-\u0A71\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD'
+ '\u0AE2-\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D'
+ '\u0B56-\u0B57\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7'
+ '\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56'
+ '\u0C82-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6'
+ '\u0D02-\u0D03\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D82-\u0D83'
+ '\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF3\u0E31\u0E34-\u0E3A'
+ '\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19'
+ '\u0F35\u0F37\u0F39\u0F3E-\u0F3F\u0F71-\u0F84\u0F86-\u0F87\u0F90-\u0F97'
+ '\u0F99-\u0FBC\u0FC6\u102C-\u1032\u1036-\u1039\u1056-\u1059\u135F'
+ '\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B6-\u17D3\u17DD'
+ '\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8-\u19C9'
+ '\u1A17-\u1A1B\u1DC0-\u1DC3\u20D0-\u20DC\u20E1\u20E5-\u20EB\u302A-\u302F'
+ '\u3099-\u309A\uA802\uA806\uA80B\uA823-\uA827\uFB1E\uFE00-\uFE0F'
+ '\uFE20-\uFE23'
// equivalent of \p{Dn}
const unicodeDigits
= '\u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F\u09E6-\u09EF'
+ '\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F'
+ '\u0CE6-\u0CEF\u0D66-\u0D6F\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29'
+ '\u1040-\u1049\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9'
+ '\uFF10-\uFF19'
// An alpha char is a unicode chars including unicode marks or
// letter or char in otherChars range
const alpha = unicodeLetters
// A numeric character is any with the number digit property, or
// underscore. These characters can be included in hashtags, but a hashtag
// cannot have only these characters.
const numeric = `${unicodeDigits}_`
// Alphanumeric char is any alpha char or a unicode char with decimal
// number property \p{Nd}
const alphanumeric = alpha + numeric
const leftChars = '{'
const rightChars = '}'
return {
alpha,
alphanumeric,
leftChars,
rightChars,
}
}
export function getHashtagRegexString(): string {
const { alpha, alphanumeric, leftChars, rightChars } = getHashtagRegexStringChars()
const hashtagAlpha = `[${alpha}]`
const hashtagAlphanumeric = `[${alphanumeric}]`
const hashLeftCharList = `[${leftChars}]`
const hashRightCharList = `[${rightChars}]`
// A hashtag contains characters, numbers and underscores,
// but not all numbers.
const hashtag
= `(${
hashLeftCharList
})`
+ `(${
hashLeftCharList
})([a-zA-Z_][a-zA-Z0-9_]{0,29}`
+ `)(${
hashRightCharList
})(${
hashRightCharList
})`
const hashtag = '(\{)(\{)([a-zA-Z_][a-zA-Z0-9_]{0,29})(\})(\})'
return hashtag
}

View File

@ -10,7 +10,7 @@ import { Line3 } from '@/app/components/base/icons/src/public/common'
type WorkflowVariableBlockComponentProps = {
nodeKey: string
variables: string[]
getWorkflowNode: (nodeId: string) => Node
getWorkflowNode: (nodeId: string) => Node | undefined
}
const WorkflowVariableBlockComponent: FC<WorkflowVariableBlockComponentProps> = ({
@ -20,7 +20,7 @@ const WorkflowVariableBlockComponent: FC<WorkflowVariableBlockComponentProps> =
}) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND)
const node = getWorkflowNode(variables[0])
const outputVarNode = node.data
const outputVarNode = node?.data
const variablesLength = variables.length
const lastVariable = variables[variablesLength - 1]

View File

@ -1,5 +1,7 @@
import type { FC } from 'react'
import { useEffect } from 'react'
import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
@ -7,6 +9,7 @@ import {
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { WorkflowVariableBlockType } from '../../types'
import {
$createWorkflowVariableBlockNode,
WorkflowVariableBlockNode,
@ -21,11 +24,11 @@ export type WorkflowVariableBlockProps = {
onInsert?: () => void
onDelete?: () => void
}
const WorkflowVariableBlock: FC<WorkflowVariableBlockProps> = ({
getWorkflowNode,
const WorkflowVariableBlock = memo(({
getWorkflowNode = () => undefined,
onInsert,
onDelete,
}) => {
}: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
@ -60,6 +63,8 @@ const WorkflowVariableBlock: FC<WorkflowVariableBlockProps> = ({
}, [editor, onInsert, onDelete, getWorkflowNode])
return null
}
})
WorkflowVariableBlock.displayName = 'WorkflowVariableBlock'
export default WorkflowVariableBlock
export { WorkflowVariableBlock }
export { WorkflowVariableBlockNode } from './node'

View File

@ -3,14 +3,15 @@ import { DecoratorNode } from 'lexical'
import WorkflowVariableBlockComponent from './component'
import type { Node } from '@/app/components/workflow/types'
type GetWorkflowNode = (nodeId: string) => Node | undefined
export type SerializedNode = SerializedLexicalNode & {
variables: string[]
getWorkflowNode: (nodeId: string) => Node
getWorkflowNode: GetWorkflowNode
}
export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
__variables: string[]
__getWorkflowNode: (nodeId: string) => Node
__getWorkflowNode: GetWorkflowNode
static getType(): string {
return 'workflow-variable-block'
@ -24,7 +25,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
return true
}
constructor(variables: string[], getWorkflowNode: (nodeId: string) => Node, key?: NodeKey) {
constructor(variables: string[], getWorkflowNode: GetWorkflowNode, key?: NodeKey) {
super(key)
this.__variables = variables
@ -70,7 +71,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
return `{{#${this.__variables.join('.')}#}}`
}
}
export function $createWorkflowVariableBlockNode(variables: string[], getWorkflowNodeName: (nodeId: string) => Node): WorkflowVariableBlockNode {
export function $createWorkflowVariableBlockNode(variables: string[], getWorkflowNodeName: (nodeId: string) => Node | undefined): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(variables, getWorkflowNodeName)
}

View File

@ -0,0 +1,63 @@
import type { Dataset } from './plugins/context-block'
import type { RoleName } from './plugins/history-block'
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
export type Option = {
value: string
name: string
}
export type ExternalToolOption = {
name: string
variableName: string
icon?: string
icon_background?: string
}
export type ContextBlockType = {
show?: boolean
selectable?: boolean
datasets?: Dataset[]
canNotAddContext?: boolean
onAddContext?: () => void
onInsert?: () => void
onDelete?: () => void
}
export type QueryBlockType = {
show?: boolean
selectable?: boolean
onInsert?: () => void
onDelete?: () => void
}
export type HistoryBlockType = {
show?: boolean
selectable?: boolean
history?: RoleName
onInsert?: () => void
onDelete?: () => void
onEditRole?: () => void
}
export type VariableBlockType = {
show?: boolean
variables?: Option[]
}
export type ExternalToolBlockType = {
show?: boolean
externalTools?: ExternalToolOption[]
onAddExternalTool?: () => void
}
export type WorkflowVariableBlockType = {
show?: boolean
variables?: NodeOutPutVar[]
getWorkflowNode?: (nodeId: string) => Node | undefined
onInsert?: () => void
onDelete?: () => void
}

View File

@ -38,7 +38,6 @@ type Props = {
const Editor: FC<Props> = ({
title,
value,
variables,
onChange,
readOnly,
showRemove,
@ -122,21 +121,12 @@ const Editor: FC<Props> = ({
className={cn('min-h-[84px]')}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
outToolDisabled
canNotAddContext
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
datasets: [],
onAddContext: () => { },
}}
variableBlock={{
variables: variables.map(item => ({
name: item,
value: item,
})),
externalTools: [],
onAddExternalTool: () => { },
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
@ -153,7 +143,6 @@ const Editor: FC<Props> = ({
}}
workflowVariableBlock={{
show: true,
selectable: true,
variables: nodesOutputVars || [],
getWorkflowNode: getNode,
}}