1+ import Button from '@/components/Button/Button' ;
12import { getClusterElementByName } from '@/pages/platform/cluster-element-editor/utils/clusterElementsUtils' ;
23import PropertyMentionsInputBubbleMenu from '@/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputBubbleMenu' ;
34import { getSuggestionOptions } from '@/pages/platform/workflow-editor/components/properties/components/property-mentions-input/propertyMentionsInputEditorSuggestionOptions' ;
@@ -20,7 +21,7 @@ import {
2021import { TYPE_ICONS } from '@/shared/typeIcons' ;
2122import { ClusterElementItemType , DataPillType } from '@/shared/types' ;
2223import { Extension , mergeAttributes } from '@tiptap/core' ;
23- import { Document } from '@tiptap/extension-document' ;
24+ import Document from '@tiptap/extension-document' ;
2425import { Mention } from '@tiptap/extension-mention' ;
2526import { Paragraph } from '@tiptap/extension-paragraph' ;
2627import { Placeholder } from '@tiptap/extension-placeholder' ;
@@ -30,6 +31,7 @@ import {EditorView} from '@tiptap/pm/view';
3031import { Editor , EditorContent , useEditor } from '@tiptap/react' ;
3132import { StarterKit } from '@tiptap/starter-kit' ;
3233import { decode } from 'html-entities' ;
34+ import { SparklesIcon , XIcon } from 'lucide-react' ;
3335import resolvePath from 'object-resolve-path' ;
3436import { ForwardedRef , MutableRefObject , forwardRef , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
3537import { renderToStaticMarkup } from 'react-dom/server' ;
@@ -39,6 +41,7 @@ import {useDebouncedCallback} from 'use-debounce';
3941import { useShallow } from 'zustand/shallow' ;
4042
4143import { FormulaMode } from './FormulaMode.extension' ;
44+ import { FromAi } from './FromAi.extension' ;
4245import { MentionStorage } from './MentionStorage.extension' ;
4346
4447interface PropertyMentionsInputEditorProps {
@@ -47,8 +50,10 @@ interface PropertyMentionsInputEditorProps {
4750 controlType ?: string ;
4851 dataPills : DataPillType [ ] ;
4952 elementId ?: string ;
50- labelId ?: string ;
53+ handleFromAiClick ?: ( fromAi : boolean ) => void ;
5154 isFormulaMode ?: boolean ;
55+ isFromAi ?: boolean ;
56+ labelId ?: string ;
5257 path ?: string ;
5358 onChange ?: ( value : string ) => void ;
5459 onFocus ?: ( editor : Editor ) => void ;
@@ -68,7 +73,9 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
6873 controlType,
6974 dataPills,
7075 elementId,
76+ handleFromAiClick,
7177 isFormulaMode,
78+ isFromAi,
7279 labelId,
7380 onChange,
7481 onFocus,
@@ -92,6 +99,7 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
9299 const pendingValueRef = useRef < string | number | null | undefined > ( undefined ) ;
93100
94101 const currentNode = useWorkflowNodeDetailsPanelStore ( ( state ) => state . currentNode ) ;
102+ const currentComponent = useWorkflowNodeDetailsPanelStore ( ( state ) => state . currentComponent ) ;
95103
96104 const getComponentIcon = useCallback (
97105 ( mentionValue : string ) => {
@@ -157,6 +165,13 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
157165 const extensions = useMemo ( ( ) => {
158166 const extensions = [
159167 ...( controlType === 'RICH_TEXT' ? [ StarterKit ] : [ Document , Paragraph , Text ] ) ,
168+ ...( memoizedClusterElementTask
169+ ? [
170+ FromAi . configure ( {
171+ setFromAi : ( ) => { } ,
172+ } ) ,
173+ ]
174+ : [ ] ) ,
160175 FormulaMode . configure ( {
161176 saveNullValue : ( ) => {
162177 if (
@@ -240,6 +255,7 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
240255 } , [
241256 controlType ,
242257 getComponentIcon ,
258+ memoizedClusterElementTask ,
243259 path ,
244260 placeholder ,
245261 setIsFormulaMode ,
@@ -283,6 +299,12 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
283299 }
284300 }
285301
302+ if ( isFromAi ) {
303+ value = `fromAi(${ path } , 'description')` ;
304+
305+ return ;
306+ }
307+
286308 const normalizedValue : string | number | null = transformedValue ? transformedValue : null ;
287309
288310 if ( normalizedValue === lastSavedRef . current || normalizedValue === pendingValueRef . current ) {
@@ -453,6 +475,7 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
453475 } ,
454476 } ,
455477 extensions,
478+ immediatelyRender : false ,
456479 onFocus : ( ) => {
457480 if ( onFocus && editor ) {
458481 onFocus ( editor ) ;
@@ -469,10 +492,33 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
469492 return getContent ( editorValue ) ;
470493 } , [ editorValue , getContent ] ) ;
471494
472- if ( ref ) {
473- ( ref as MutableRefObject < Editor | null > ) . current = editor ;
474- }
495+ const fromAiExtension = useMemo (
496+ ( ) => editor ?. extensionManager . extensions . find ( ( extension ) => extension . name === 'fromAi' ) ,
497+ [ editor ]
498+ ) ;
475499
500+ // Sync ref when editor changes - handle both callback and object refs
501+ useEffect ( ( ) => {
502+ if ( ! ref ) {
503+ return ;
504+ }
505+
506+ if ( typeof ref === 'function' ) {
507+ ref ( editor ) ;
508+ } else if ( ref && 'current' in ref ) {
509+ ( ref as MutableRefObject < Editor | null > ) . current = editor ;
510+ }
511+
512+ return ( ) => {
513+ if ( typeof ref === 'function' ) {
514+ ref ( null ) ;
515+ } else if ( ref && 'current' in ref ) {
516+ ( ref as MutableRefObject < Editor | null > ) . current = null ;
517+ }
518+ } ;
519+ } , [ editor , ref ] ) ;
520+
521+ // Update data pills in MentionStorage when they change
476522 useEffect ( ( ) => {
477523 if ( editor ) {
478524 editor . storage . MentionStorage . dataPills = dataPills ;
@@ -542,6 +588,28 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
542588 }
543589 } , [ editor , value , isFormulaMode , setIsFormulaMode ] ) ;
544590
591+ // Set fromAi based on metadata and sync with editor storage
592+ useEffect ( ( ) => {
593+ if ( ! editor || ! path || isFromAi === undefined || ! currentComponent ?. metadata ?. ui ?. fromAi ?. includes ( path ) ) {
594+ return ;
595+ }
596+
597+ if ( currentComponent ?. metadata ?. ui ?. fromAi ?. includes ( path ) ) {
598+ editor . commands . setFromAi ( isFromAi ) ;
599+ }
600+ } , [ currentComponent , currentComponent ?. metadata ?. ui ?. fromAi , editor , isFromAi , path ] ) ;
601+
602+ // Set editable based on isFromAi
603+ useEffect ( ( ) => {
604+ if ( path && ! currentComponent ?. metadata ?. ui ?. fromAi ?. includes ( path ) ) {
605+ return ;
606+ }
607+
608+ if ( editor && isFromAi !== undefined ) {
609+ editor . setEditable ( ! isFromAi ) ;
610+ }
611+ } , [ currentComponent ?. metadata ?. ui ?. fromAi , editor , isFromAi , path ] ) ;
612+
545613 // Cleanup function to save mention input value on unmount
546614 useEffect ( ( ) => {
547615 return ( ) => saveMentionInputValue . flush ( ) ;
@@ -550,11 +618,33 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
550618 return (
551619 < >
552620 < EditorContent
621+ className = { twMerge ( isFromAi && 'pointer-events-none cursor-not-allowed' ) }
622+ disabled = { isFromAi }
553623 editor = { editor }
554624 onChange = { ( event ) => setEditorValue ( ( event . target as HTMLInputElement ) . value ) }
555625 value = { editorValue }
556626 />
557627
628+ { fromAiExtension &&
629+ ( isFromAi ? (
630+ < Button
631+ className = "self-center"
632+ icon = { < XIcon /> }
633+ onClick = { ( ) => handleFromAiClick && handleFromAiClick ( false ) }
634+ size = "iconSm"
635+ title = "Stop AI generation"
636+ variant = "destructiveGhost"
637+ />
638+ ) : (
639+ < Button
640+ className = "self-center"
641+ icon = { < SparklesIcon /> }
642+ onClick = { ( ) => handleFromAiClick && handleFromAiClick ( true ) }
643+ size = "iconSm"
644+ title = "Generate content with AI"
645+ variant = "ghost"
646+ />
647+ ) ) }
558648 { controlType === 'RICH_TEXT' && editor && < PropertyMentionsInputBubbleMenu editor = { editor } /> }
559649 </ >
560650 ) ;
0 commit comments