Skip to content

Commit c9e52f1

Browse files
committed
2893 - add fromAi handling to cluster element properties
1 parent 068b364 commit c9e52f1

File tree

8 files changed

+229
-10
lines changed

8 files changed

+229
-10
lines changed

client/src/pages/platform/workflow-editor/components/properties/Property.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const Property = ({
7979
formattedOptions,
8080
handleCodeEditorChange,
8181
handleDeleteCustomPropertyClick,
82+
handleFromAiClick,
8283
handleInputChange,
8384
handleInputTypeSwitchButtonClick,
8485
handleJsonSchemaBuilderChange,
@@ -91,6 +92,7 @@ const Property = ({
9192
isDisplayConditionsPending,
9293
isFetchingCurrentDisplayCondition,
9394
isFormulaMode,
95+
isFromAi,
9496
isNumericalInput,
9597
isValidControlType,
9698
label,
@@ -185,8 +187,10 @@ const Property = ({
185187
defaultValue={defaultValue}
186188
deletePropertyButton={deletePropertyButton}
187189
description={description}
190+
handleFromAiClick={handleFromAiClick}
188191
handleInputTypeSwitchButtonClick={handleInputTypeSwitchButtonClick}
189192
isFormulaMode={isFormulaMode}
193+
isFromAi={isFromAi}
190194
label={label || name}
191195
leadingIcon={typeIcon}
192196
path={calculatedPath}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {Extension} from '@tiptap/core';
2+
3+
export interface FromAiOptionsI {
4+
setFromAi: (value: boolean) => void;
5+
}
6+
7+
declare module '@tiptap/core' {
8+
// eslint-disable-next-line @typescript-eslint/naming-convention
9+
interface Commands<ReturnType> {
10+
fromAi: {
11+
setFromAi: (value: boolean) => ReturnType;
12+
};
13+
}
14+
}
15+
16+
export const FromAi = Extension.create<FromAiOptionsI>({
17+
addCommands() {
18+
return {
19+
setFromAi: (value: boolean) => () => {
20+
this.storage.fromAi = value;
21+
22+
return true;
23+
},
24+
};
25+
},
26+
addOptions() {
27+
return {
28+
setFromAi: () => {},
29+
};
30+
},
31+
addStorage() {
32+
return {
33+
fromAi: false,
34+
};
35+
},
36+
name: 'fromAi',
37+
});

client/src/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInput.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,12 @@
1313
@apply pointer-events-none;
1414
}
1515
}
16+
17+
&.is-from-ai {
18+
.tiptap {
19+
p {
20+
@apply inline-flex items-center rounded-full bg-muted px-2 py-0.5 font-mono text-sm leading-none;
21+
}
22+
}
23+
}
1624
}

client/src/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInput.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ interface PropertyMentionsInputProps {
4141
defaultValue?: string;
4242
deletePropertyButton?: ReactNode;
4343
description?: string;
44+
handleFromAiClick?: (fromAi: boolean) => void;
4445
handleInputTypeSwitchButtonClick?: () => void;
46+
isFromAi?: boolean;
4547
isFormulaMode?: boolean;
4648
label?: string;
4749
leadingIcon?: ReactNode;
@@ -62,8 +64,10 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
6264
defaultValue,
6365
deletePropertyButton,
6466
description,
67+
handleFromAiClick,
6568
handleInputTypeSwitchButtonClick,
6669
isFormulaMode,
70+
isFromAi,
6771
label,
6872
leadingIcon,
6973
path,
@@ -134,14 +138,21 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
134138
localEditorRef.current = instance;
135139

136140
if (typeof ref === 'function') {
137-
ref(instance as Editor | null);
141+
ref(instance);
138142
} else if (ref && 'current' in ref) {
139143
ref.current = instance;
140144
}
141145
},
142146
[ref]
143147
);
144148

149+
// Ensure localEditorRef stays in sync with parent ref
150+
useEffect(() => {
151+
if (ref && typeof ref !== 'function' && 'current' in ref && ref.current && !localEditorRef.current) {
152+
localEditorRef.current = ref.current;
153+
}
154+
}, [ref]);
155+
145156
useEffect(() => {
146157
if (!focusedInput || !localEditorRef.current) {
147158
setIsFocused(false);
@@ -239,6 +250,7 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
239250
className={twMerge(
240251
'property-mentions-editor flex h-full min-h-[34px] w-full rounded-md bg-white',
241252
leadingIcon && 'border-0 pl-10 pr-0.5',
253+
isFromAi && 'is-from-ai',
242254
className
243255
)}
244256
>
@@ -248,7 +260,9 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
248260
controlType={controlType}
249261
dataPills={dataPills}
250262
elementId={elementId}
263+
handleFromAiClick={handleFromAiClick}
251264
isFormulaMode={isFormulaMode}
265+
isFromAi={isFromAi}
252266
labelId={labelId}
253267
onChange={(value) => handleEditorValueChange(value)}
254268
onFocus={onFocus}

client/src/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputEditor.tsx

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Button from '@/components/Button/Button';
12
import {getClusterElementByName} from '@/pages/platform/cluster-element-editor/utils/clusterElementsUtils';
23
import PropertyMentionsInputBubbleMenu from '@/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputBubbleMenu';
34
import {getSuggestionOptions} from '@/pages/platform/workflow-editor/components/properties/components/property-mentions-input/propertyMentionsInputEditorSuggestionOptions';
@@ -20,7 +21,7 @@ import {
2021
import {TYPE_ICONS} from '@/shared/typeIcons';
2122
import {ClusterElementItemType, DataPillType} from '@/shared/types';
2223
import {Extension, mergeAttributes} from '@tiptap/core';
23-
import {Document} from '@tiptap/extension-document';
24+
import Document from '@tiptap/extension-document';
2425
import {Mention} from '@tiptap/extension-mention';
2526
import {Paragraph} from '@tiptap/extension-paragraph';
2627
import {Placeholder} from '@tiptap/extension-placeholder';
@@ -30,6 +31,7 @@ import {EditorView} from '@tiptap/pm/view';
3031
import {Editor, EditorContent, useEditor} from '@tiptap/react';
3132
import {StarterKit} from '@tiptap/starter-kit';
3233
import {decode} from 'html-entities';
34+
import {SparklesIcon, XIcon} from 'lucide-react';
3335
import resolvePath from 'object-resolve-path';
3436
import {ForwardedRef, MutableRefObject, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
3537
import {renderToStaticMarkup} from 'react-dom/server';
@@ -39,6 +41,7 @@ import {useDebouncedCallback} from 'use-debounce';
3941
import {useShallow} from 'zustand/shallow';
4042

4143
import {FormulaMode} from './FormulaMode.extension';
44+
import {FromAi} from './FromAi.extension';
4245
import {MentionStorage} from './MentionStorage.extension';
4346

4447
interface 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

Comments
 (0)