Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const Property = ({
formattedOptions,
handleCodeEditorChange,
handleDeleteCustomPropertyClick,
handleFromAiClick,
handleInputChange,
handleInputTypeSwitchButtonClick,
handleJsonSchemaBuilderChange,
Expand All @@ -91,6 +92,7 @@ const Property = ({
isDisplayConditionsPending,
isFetchingCurrentDisplayCondition,
isFormulaMode,
isFromAi,
isNumericalInput,
isValidControlType,
label,
Expand Down Expand Up @@ -185,8 +187,10 @@ const Property = ({
defaultValue={defaultValue}
deletePropertyButton={deletePropertyButton}
description={description}
handleFromAiClick={handleFromAiClick}
handleInputTypeSwitchButtonClick={handleInputTypeSwitchButtonClick}
isFormulaMode={isFormulaMode}
isFromAi={isFromAi}
label={label || name}
leadingIcon={typeIcon}
path={calculatedPath}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {Extension} from '@tiptap/core';

export interface FromAiOptionsI {
setFromAi: (value: boolean) => void;
}

declare module '@tiptap/core' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Commands<ReturnType> {
fromAi: {
setFromAi: (value: boolean) => ReturnType;
};
}
}

export const FromAi = Extension.create<FromAiOptionsI>({
addCommands() {
return {
setFromAi: (value: boolean) => () => {
this.storage.fromAi = value;

return true;
},
};
},
addOptions() {
return {
setFromAi: () => {},
};
},
addStorage() {
return {
fromAi: false,
};
},
name: 'fromAi',
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@
@apply pointer-events-none;
}
}

&.is-from-ai {
.tiptap {
p {
@apply inline-flex items-center rounded-full bg-muted px-2 py-0.5 font-mono text-sm leading-none;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ interface PropertyMentionsInputProps {
defaultValue?: string;
deletePropertyButton?: ReactNode;
description?: string;
handleFromAiClick?: (fromAi: boolean) => void;
handleInputTypeSwitchButtonClick?: () => void;
isFromAi?: boolean;
isFormulaMode?: boolean;
label?: string;
leadingIcon?: ReactNode;
Expand All @@ -62,8 +64,10 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
defaultValue,
deletePropertyButton,
description,
handleFromAiClick,
handleInputTypeSwitchButtonClick,
isFormulaMode,
isFromAi,
label,
leadingIcon,
path,
Expand Down Expand Up @@ -134,14 +138,21 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
localEditorRef.current = instance;

if (typeof ref === 'function') {
ref(instance as Editor | null);
ref(instance);
} else if (ref && 'current' in ref) {
ref.current = instance;
}
},
[ref]
);

// Ensure localEditorRef stays in sync with parent ref
useEffect(() => {
if (ref && typeof ref !== 'function' && 'current' in ref && ref.current && !localEditorRef.current) {
localEditorRef.current = ref.current;
}
}, [ref]);

useEffect(() => {
if (!focusedInput || !localEditorRef.current) {
setIsFocused(false);
Expand Down Expand Up @@ -239,6 +250,7 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
className={twMerge(
'property-mentions-editor flex h-full min-h-[34px] w-full rounded-md bg-white',
leadingIcon && 'border-0 pl-10 pr-0.5',
isFromAi && 'is-from-ai',
className
)}
>
Expand All @@ -248,7 +260,9 @@ const PropertyMentionsInput = forwardRef<Editor, PropertyMentionsInputProps>(
controlType={controlType}
dataPills={dataPills}
elementId={elementId}
handleFromAiClick={handleFromAiClick}
isFormulaMode={isFormulaMode}
isFromAi={isFromAi}
labelId={labelId}
onChange={(value) => handleEditorValueChange(value)}
onFocus={onFocus}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Button from '@/components/Button/Button';
import {getClusterElementByName} from '@/pages/platform/cluster-element-editor/utils/clusterElementsUtils';
import PropertyMentionsInputBubbleMenu from '@/pages/platform/workflow-editor/components/properties/components/property-mentions-input/PropertyMentionsInputBubbleMenu';
import {getSuggestionOptions} from '@/pages/platform/workflow-editor/components/properties/components/property-mentions-input/propertyMentionsInputEditorSuggestionOptions';
Expand All @@ -20,7 +21,7 @@ import {
import {TYPE_ICONS} from '@/shared/typeIcons';
import {ClusterElementItemType, DataPillType} from '@/shared/types';
import {Extension, mergeAttributes} from '@tiptap/core';
import {Document} from '@tiptap/extension-document';
import Document from '@tiptap/extension-document';
import {Mention} from '@tiptap/extension-mention';
import {Paragraph} from '@tiptap/extension-paragraph';
import {Placeholder} from '@tiptap/extension-placeholder';
Expand All @@ -30,6 +31,7 @@ import {EditorView} from '@tiptap/pm/view';
import {Editor, EditorContent, useEditor} from '@tiptap/react';
import {StarterKit} from '@tiptap/starter-kit';
import {decode} from 'html-entities';
import {SparklesIcon, XIcon} from 'lucide-react';
import resolvePath from 'object-resolve-path';
import {ForwardedRef, MutableRefObject, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
Expand All @@ -39,6 +41,7 @@ import {useDebouncedCallback} from 'use-debounce';
import {useShallow} from 'zustand/shallow';

import {FormulaMode} from './FormulaMode.extension';
import {FromAi} from './FromAi.extension';
import {MentionStorage} from './MentionStorage.extension';

interface PropertyMentionsInputEditorProps {
Expand All @@ -47,8 +50,10 @@ interface PropertyMentionsInputEditorProps {
controlType?: string;
dataPills: DataPillType[];
elementId?: string;
labelId?: string;
handleFromAiClick?: (fromAi: boolean) => void;
isFormulaMode?: boolean;
isFromAi?: boolean;
labelId?: string;
path?: string;
onChange?: (value: string) => void;
onFocus?: (editor: Editor) => void;
Expand All @@ -68,7 +73,9 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
controlType,
dataPills,
elementId,
handleFromAiClick,
isFormulaMode,
isFromAi,
labelId,
onChange,
onFocus,
Expand All @@ -92,6 +99,7 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
const pendingValueRef = useRef<string | number | null | undefined>(undefined);

const currentNode = useWorkflowNodeDetailsPanelStore((state) => state.currentNode);
const currentComponent = useWorkflowNodeDetailsPanelStore((state) => state.currentComponent);

const getComponentIcon = useCallback(
(mentionValue: string) => {
Expand Down Expand Up @@ -157,6 +165,13 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
const extensions = useMemo(() => {
const extensions = [
...(controlType === 'RICH_TEXT' ? [StarterKit] : [Document, Paragraph, Text]),
...(memoizedClusterElementTask
? [
FromAi.configure({
setFromAi: () => {},
}),
]
: []),
FormulaMode.configure({
saveNullValue: () => {
if (
Expand Down Expand Up @@ -239,6 +254,7 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
}, [
controlType,
getComponentIcon,
memoizedClusterElementTask,
path,
placeholder,
setIsFormulaMode,
Expand Down Expand Up @@ -282,6 +298,12 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
}
}

if (isFromAi) {
value = `fromAi(${path}, 'description')`;

return;
}

const normalizedValue: string | number | null = transformedValue ? transformedValue : null;

if (normalizedValue === lastSavedRef.current || normalizedValue === pendingValueRef.current) {
Expand Down Expand Up @@ -452,6 +474,7 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
},
},
extensions,
immediatelyRender: false,
onFocus: () => {
if (onFocus && editor) {
onFocus(editor);
Expand All @@ -468,10 +491,33 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
return getContent(editorValue);
}, [editorValue, getContent]);

if (ref) {
(ref as MutableRefObject<Editor | null>).current = editor;
}
const fromAiExtension = useMemo(
() => editor?.extensionManager.extensions.find((extension) => extension.name === 'fromAi'),
[editor]
);

// Sync ref when editor changes - handle both callback and object refs
useEffect(() => {
if (!ref) {
return;
}

if (typeof ref === 'function') {
ref(editor);
} else if (ref && 'current' in ref) {
(ref as MutableRefObject<Editor | null>).current = editor;
}

return () => {
if (typeof ref === 'function') {
ref(null);
} else if (ref && 'current' in ref) {
(ref as MutableRefObject<Editor | null>).current = null;
}
};
}, [editor, ref]);

// Update data pills in MentionStorage when they change
useEffect(() => {
if (editor) {
editor.storage.MentionStorage.dataPills = dataPills;
Expand Down Expand Up @@ -541,6 +587,28 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
}
}, [editor, value, isFormulaMode, setIsFormulaMode]);

// Set fromAi based on metadata and sync with editor storage
useEffect(() => {
if (!editor || !path || isFromAi === undefined || !currentComponent?.metadata?.ui?.fromAi?.includes(path)) {
return;
}

if (currentComponent?.metadata?.ui?.fromAi?.includes(path)) {
editor.commands.setFromAi(isFromAi);
}
}, [currentComponent, currentComponent?.metadata?.ui?.fromAi, editor, isFromAi, path]);

// Set editable based on isFromAi
useEffect(() => {
if (path && !currentComponent?.metadata?.ui?.fromAi?.includes(path)) {
return;
}

if (editor && isFromAi !== undefined) {
editor.setEditable(!isFromAi);
}
}, [currentComponent?.metadata?.ui?.fromAi, editor, isFromAi, path]);

// Cleanup function to save mention input value on unmount
useEffect(() => {
return () => saveMentionInputValue.flush();
Expand All @@ -549,11 +617,33 @@ const PropertyMentionsInputEditor = forwardRef<Editor, PropertyMentionsInputEdit
return (
<>
<EditorContent
className={twMerge(isFromAi && 'pointer-events-none cursor-not-allowed')}
disabled={isFromAi}
editor={editor}
onChange={(event) => setEditorValue((event.target as HTMLInputElement).value)}
value={editorValue}
/>

{fromAiExtension &&
(isFromAi ? (
<Button
className="self-center"
icon={<XIcon />}
onClick={() => handleFromAiClick && handleFromAiClick(false)}
size="iconSm"
title="Stop AI generation"
variant="destructiveGhost"
/>
) : (
<Button
className="self-center"
icon={<SparklesIcon />}
onClick={() => handleFromAiClick && handleFromAiClick(true)}
size="iconSm"
title="Generate content with AI"
variant="ghost"
/>
))}
{controlType === 'RICH_TEXT' && editor && <PropertyMentionsInputBubbleMenu editor={editor} />}
</>
);
Expand Down
Loading
Loading