'use client' import type { InitialConfigType } from '@lexical/react/LexicalComposer' import type { EditorState, LexicalCommand, } from 'lexical' import type { FC } from 'react' import type { Hotkey } from './plugins/shortcuts-popup-plugin' import type { AgentBlockType, ContextBlockType, CurrentBlockType, ErrorMessageBlockType, ExternalToolBlockType, HistoryBlockType, HITLInputBlockType, LastRunBlockType, QueryBlockType, RequestURLBlockType, VariableBlockType, WorkflowVariableBlockType, } from './types' import type { Node as WorkflowNode } from '@/app/components/workflow/types' import type { EventPayload } from '@/context/event-emitter' import { CodeNode } from '@lexical/code' import { LexicalComposer } from '@lexical/react/LexicalComposer' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { ContentEditable } from '@lexical/react/LexicalContentEditable' import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' import { $getRoot, COMMAND_PRIORITY_LOW, KEY_ENTER_COMMAND, TextNode, } from 'lexical' import * as React from 'react' import { useEffect, useState } from 'react' import { WorkflowContext } from '@/app/components/workflow/context' import { HooksStoreContext } from '@/app/components/workflow/hooks-store/provider' import { FileReferenceNode } from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/node' import { FilePreviewContextProvider } from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/preview-context' import FileReferenceReplacementBlock from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/replacement-block' import { ToolBlock, ToolBlockNode, ToolBlockReplacementBlock, ToolGroupBlockNode, ToolGroupBlockReplacementBlock, } from '@/app/components/workflow/skill/editor/skill-editor/plugins/tool-block' import { ToolBlockContextProvider } from '@/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/tool-block-context' import ToolPickerBlock from '@/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/tool-picker-block' import { useEventEmitterContextContext } from '@/context/event-emitter' import { cn } from '@/utils/classnames' import { useWorkflow } from '../../workflow/hooks' import { UPDATE_DATASETS_EVENT_EMITTER, UPDATE_HISTORY_EVENT_EMITTER, } from './constants' import ComponentPickerBlock from './plugins/component-picker-block' import { ContextBlock, ContextBlockNode, ContextBlockReplacementBlock, } from './plugins/context-block' import { CurrentBlock, CurrentBlockNode, CurrentBlockReplacementBlock, } from './plugins/current-block' import { CustomTextNode } from './plugins/custom-text/node' import DraggableBlockPlugin from './plugins/draggable-plugin' import { ErrorMessageBlock, ErrorMessageBlockNode, ErrorMessageBlockReplacementBlock, } from './plugins/error-message-block' import { HistoryBlock, HistoryBlockNode, HistoryBlockReplacementBlock, } from './plugins/history-block' import { HITLInputBlock, HITLInputBlockReplacementBlock, HITLInputNode, } from './plugins/hitl-input-block' import { LastRunBlock, LastRunBlockNode, LastRunReplacementBlock, } from './plugins/last-run-block' import OnBlurBlock from './plugins/on-blur-or-focus-block' // import TreeView from './plugins/tree-view' import Placeholder from './plugins/placeholder' import { QueryBlock, QueryBlockNode, QueryBlockReplacementBlock, } from './plugins/query-block' import { RequestURLBlock, RequestURLBlockNode, RequestURLBlockReplacementBlock, } from './plugins/request-url-block' import ShortcutsPopupPlugin from './plugins/shortcuts-popup-plugin' import UpdateBlock from './plugins/update-block' import VariableBlock from './plugins/variable-block' import VariableValueBlock from './plugins/variable-value-block' import { VariableValueBlockNode } from './plugins/variable-value-block/node' import { WorkflowVariableBlock, WorkflowVariableBlockNode, WorkflowVariableBlockReplacementBlock, } from './plugins/workflow-variable-block' import SandboxPlaceholder from './sandbox-placeholder' import { textToEditorState } from './utils' const ValueSyncPlugin: FC<{ value?: string }> = ({ value }) => { const [editor] = useLexicalComposerContext() useEffect(() => { if (value === undefined) return const incomingValue = value ?? '' const shouldUpdate = editor.getEditorState().read(() => { const currentText = $getRoot().getChildren().map(node => node.getTextContent()).join('\n') return currentText !== incomingValue }) if (!shouldUpdate) return const editorState = editor.parseEditorState(textToEditorState(incomingValue)) editor.setEditorState(editorState) editor.update(() => { $getRoot().getAllTextNodes().forEach((node) => { if (node instanceof CustomTextNode) node.markDirty() }) }) }, [editor, value]) return null } const EnterCommandPlugin: FC<{ onEnter?: (event: KeyboardEvent) => void }> = ({ onEnter }) => { const [editor] = useLexicalComposerContext() useEffect(() => { if (!onEnter) return return editor.registerCommand( KEY_ENTER_COMMAND, (event: KeyboardEvent) => { if (!event || event.defaultPrevented) return false if (event.isComposing || event.shiftKey) return false event.preventDefault() onEnter(event) return true }, COMMAND_PRIORITY_LOW, ) }, [editor, onEnter]) return null } type WorkflowAvailableNodesProps = { nodeId?: string isSupportSandbox?: boolean children: (availableNodes: WorkflowNode[]) => React.ReactNode } const WorkflowAvailableNodes: FC = ({ nodeId, isSupportSandbox, children, }) => { const { getBeforeNodesInSameBranch } = useWorkflow() const availableNodes = React.useMemo( () => nodeId && isSupportSandbox ? getBeforeNodesInSameBranch(nodeId || '') : [], [getBeforeNodesInSameBranch, isSupportSandbox, nodeId], ) return ( <> {children(availableNodes)} ) } export type PromptEditorProps = { instanceId?: string nodeId?: string compact?: boolean wrapperClassName?: string className?: string placeholder?: string | React.ReactNode placeholderClassName?: string style?: React.CSSProperties value?: string editable?: boolean onChange?: (text: string) => void onBlur?: () => void onFocus?: () => void toolMetadata?: Record onToolMetadataChange?: (metadata: Record) => void contextBlock?: ContextBlockType queryBlock?: QueryBlockType requestURLBlock?: RequestURLBlockType historyBlock?: HistoryBlockType variableBlock?: VariableBlockType externalToolBlock?: ExternalToolBlockType workflowVariableBlock?: WorkflowVariableBlockType hitlInputBlock?: HITLInputBlockType currentBlock?: CurrentBlockType errorMessageBlock?: ErrorMessageBlockType lastRunBlock?: LastRunBlockType agentBlock?: AgentBlockType isSupportFileVar?: boolean isSupportSandbox?: boolean disableToolBlocks?: boolean onEnter?: (event: KeyboardEvent) => void shortcutPopups?: Array<{ hotkey: Hotkey, Popup: React.ComponentType<{ onClose: () => void, onInsert: (command: LexicalCommand, params: unknown[]) => void }> }> } type PromptEditorContentProps = PromptEditorProps & { availableNodes: WorkflowNode[] } const PromptEditorContent: FC = ({ instanceId, nodeId, compact, wrapperClassName, className, placeholder, placeholderClassName, style, value, editable = true, onChange, onBlur, onFocus, toolMetadata, onToolMetadataChange, contextBlock, queryBlock, requestURLBlock, historyBlock, variableBlock, externalToolBlock, workflowVariableBlock, hitlInputBlock, currentBlock, errorMessageBlock, lastRunBlock, agentBlock, isSupportFileVar, isSupportSandbox, disableToolBlocks, onEnter, shortcutPopups = [], availableNodes, }) => { const { eventEmitter } = useEventEmitterContextContext() const initialConfig: InitialConfigType = { theme: { paragraph: 'group-[.clamp]:line-clamp-5 group-focus/editable:!line-clamp-none', }, namespace: 'prompt-editor', nodes: [ CodeNode, CustomTextNode, { replace: TextNode, with: (node: TextNode) => new CustomTextNode(node.__text), withKlass: CustomTextNode, }, ContextBlockNode, HistoryBlockNode, QueryBlockNode, RequestURLBlockNode, WorkflowVariableBlockNode, VariableValueBlockNode, HITLInputNode, CurrentBlockNode, ErrorMessageBlockNode, LastRunBlockNode, // LastRunBlockNode is used for error message block replacement ...(isSupportSandbox ? [FileReferenceNode, ToolGroupBlockNode, ToolBlockNode] : []), ], editorState: textToEditorState(value || ''), onError: (error: Error) => { throw error }, } const handleEditorChange = (editorState: EditorState) => { const text = editorState.read(() => { return $getRoot().getChildren().map(p => p.getTextContent()).join('\n') }) if (onChange) onChange(text) } useEffect(() => { eventEmitter?.emit({ type: UPDATE_DATASETS_EVENT_EMITTER, payload: contextBlock?.datasets, } as EventPayload) }, [eventEmitter, contextBlock?.datasets]) useEffect(() => { eventEmitter?.emit({ type: UPDATE_HISTORY_EVENT_EMITTER, payload: historyBlock?.history, } as EventPayload) }, [eventEmitter, historyBlock?.history]) const toolBlockContextValue = React.useMemo(() => { if (!onToolMetadataChange) return null return { metadata: toolMetadata, onMetadataChange: onToolMetadataChange, useModal: true, disableToolBlocks, nodeId, nodesOutputVars: workflowVariableBlock?.variables, availableNodes, } }, [availableNodes, disableToolBlocks, nodeId, onToolMetadataChange, toolMetadata, workflowVariableBlock?.variables]) const filePreviewContextValue = React.useMemo(() => ({ enabled: Boolean(isSupportSandbox), }), [isSupportSandbox]) const [floatingAnchorElem, setFloatingAnchorElem] = useState(null) const onRef = (floatingAnchorElement: HTMLDivElement | null) => { if (floatingAnchorElement !== null) setFloatingAnchorElem(floatingAnchorElement) } return (
)} placeholder={( )} className={cn('truncate', placeholderClassName)} compact={compact} /> )} ErrorBoundary={LexicalErrorBoundary} /> {shortcutPopups?.map(({ hotkey, Popup }, idx) => ( {(closePortal, onInsert) => } ))} {!isSupportSandbox && (!agentBlock || agentBlock.show) && ( )} {isSupportSandbox && ( <> {editable && !disableToolBlocks && } )} { contextBlock?.show && ( <> ) } { queryBlock?.show && ( <> ) } { historyBlock?.show && ( <> ) } { (variableBlock?.show || externalToolBlock?.show) && ( <> ) } { workflowVariableBlock?.show && ( <> ) } { hitlInputBlock?.show && ( <> ) } {isSupportSandbox && } { currentBlock?.show && ( <> ) } { requestURLBlock?.show && ( <> ) } { errorMessageBlock?.show && ( <> ) } { lastRunBlock?.show && ( <> ) } { isSupportFileVar && ( ) } {floatingAnchorElem && ( )} {/* */}
) } const PromptEditor: FC = (props) => { const workflowStore = React.useContext(WorkflowContext) const hooksStore = React.useContext(HooksStoreContext) const hasWorkflowContext = Boolean(workflowStore && hooksStore) if (!hasWorkflowContext) { return ( ) } return ( {availableNodes => ( )} ) } export default PromptEditor