Skip to content

Commit 13f8533

Browse files
authored
Merge pull request #1912 from giselles-ai/feat/suggestion-input
feat: Add mention/suggestion functionality to text editor
2 parents 2867ea8 + 4989f0a commit 13f8533

File tree

10 files changed

+364
-17
lines changed

10 files changed

+364
-17
lines changed

docs/packages-license.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
## Summary
5-
* 858 MIT
5+
* 860 MIT
66
* 188 Apache 2.0
77
* 46 ISC
88
* 27 New BSD
@@ -4262,6 +4262,17 @@ LGPL-3.0-or-later permitted
42624262

42634263

42644264

4265+
<a name="@tiptap/extension-mention"></a>
4266+
### @tiptap/extension-mention v2.11.5
4267+
####
4268+
4269+
##### Paths
4270+
* /home/runner/work/giselle/giselle
4271+
4272+
<a href="http://opensource.org/licenses/mit-license">MIT</a> permitted
4273+
4274+
4275+
42654276
<a name="@tiptap/extension-ordered-list"></a>
42664277
### @tiptap/extension-ordered-list v2.11.5
42674278
####
@@ -4372,6 +4383,17 @@ LGPL-3.0-or-later permitted
43724383

43734384

43744385

4386+
<a name="@tiptap/suggestion"></a>
4387+
### @tiptap/suggestion v2.11.5
4388+
####
4389+
4390+
##### Paths
4391+
* /home/runner/work/giselle/giselle
4392+
4393+
<a href="http://opensource.org/licenses/mit-license">MIT</a> permitted
4394+
4395+
4396+
43754397
<a name="@trigger.dev/core"></a>
43764398
### @trigger.dev/core v4.0.4
43774399
####

internal-packages/workflow-designer-ui/src/editor/properties-panel/image-generation-node-properties-panel/prompt-panel.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export function PromptPanel({ node }: { node: ImageGenerationNode }) {
2727
updateNodeDataContent(node, { prompt: value });
2828
}}
2929
nodes={nodes}
30+
connectedSources={connectedSources.map(({ node, output }) => ({
31+
node,
32+
output,
33+
}))}
3034
tools={(editor) => (
3135
<DropdownMenu
3236
trigger={<AtSignIcon className="w-[18px]" />}

internal-packages/workflow-designer-ui/src/editor/properties-panel/query-node-properties-panel/query-panel.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ export function QueryPanel({ node }: { node: QueryNode }) {
9999
updateNodeDataContent(node, { query: value });
100100
}}
101101
nodes={connectedInputsWithoutDatasource.map((input) => input.node)}
102+
connectedSources={connectedInputsWithoutDatasource.map(
103+
({ node, output }) => ({
104+
node,
105+
output,
106+
}),
107+
)}
102108
header={
103109
connectedDatasourceInputs.length > 0 ? (
104110
<div className="flex items-center gap-[6px] flex-wrap">

internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/prompt-panel.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ export function PromptPanel({ node }: { node: TextGenerationNode }) {
2626
updateNodeDataContent(node, { prompt: value });
2727
}}
2828
nodes={connectedSources.map((source) => source.node)}
29+
connectedSources={connectedSources.map(
30+
({ node, id, label, accessor }) => ({
31+
node,
32+
output: {
33+
id,
34+
label,
35+
accessor,
36+
},
37+
}),
38+
)}
2939
tools={(editor) => (
3040
<DropdownMenu
3141
trigger={<AtSignIcon className="w-[18px]" />}

packages/text-editor/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
"@giselle-sdk/data-type": "workspace:^",
3535
"@giselle-sdk/giselle": "workspace:^",
3636
"@giselle-sdk/text-editor-utils": "workspace:^",
37+
"@tiptap/extension-mention": "catalog:",
3738
"@tiptap/extension-placeholder": "catalog:",
3839
"@tiptap/react": "catalog:",
40+
"@tiptap/suggestion": "catalog:",
3941
"clsx": "catalog:",
4042
"lucide-react": "catalog:",
4143
"radix-ui": "catalog:",

packages/text-editor/src/react/component.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import type { Node } from "@giselle-sdk/data-type";
1+
import type { Node, Output } from "@giselle-sdk/data-type";
22
import { extensions as baseExtensions } from "@giselle-sdk/text-editor-utils";
3+
import Mention from "@tiptap/extension-mention";
34
import Placeholder from "@tiptap/extension-placeholder";
45
import { type Editor, EditorProvider, useCurrentEditor } from "@tiptap/react";
56
import clsx from "clsx/lite";
@@ -13,6 +14,7 @@ import {
1314
import { Toolbar as ToolbarPrimitive } from "radix-ui";
1415
import { type ReactNode, useMemo } from "react";
1516
import { SourceExtensionReact } from "./source-extension-react";
17+
import { createSuggestion } from "./suggestion";
1618

1719
function Toolbar({ tools }: { tools?: (editor: Editor) => ReactNode }) {
1820
const { editor } = useCurrentEditor();
@@ -119,32 +121,48 @@ function Toolbar({ tools }: { tools?: (editor: Editor) => ReactNode }) {
119121
);
120122
}
121123

124+
export interface ConnectedSource {
125+
node: Node;
126+
output: Output;
127+
}
128+
122129
export function TextEditor({
123130
value,
124131
onValueChange,
125132
tools,
126133
nodes,
134+
connectedSources,
127135
placeholder,
128136
header,
129137
}: {
130138
value?: string;
131139
onValueChange?: (value: string) => void;
132140
tools?: (editor: Editor) => ReactNode;
133141
nodes?: Node[];
142+
connectedSources?: ConnectedSource[];
134143
placeholder?: string;
135144
header?: ReactNode;
136145
}) {
137146
const extensions = useMemo(() => {
147+
const mentionExtension = Mention.configure({
148+
suggestion: createSuggestion(connectedSources),
149+
});
150+
138151
return nodes === undefined
139-
? [...baseExtensions, Placeholder.configure({ placeholder })]
152+
? [
153+
...baseExtensions,
154+
mentionExtension,
155+
Placeholder.configure({ placeholder }),
156+
]
140157
: [
141158
...baseExtensions,
142159
SourceExtensionReact.configure({
143160
nodes,
144161
}),
162+
mentionExtension,
145163
Placeholder.configure({ placeholder }),
146164
];
147-
}, [nodes, placeholder]);
165+
}, [nodes, connectedSources, placeholder]);
148166
return (
149167
<div className="flex flex-col h-full w-full">
150168
<EditorProvider
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { Node as GiselleNode, Output } from "@giselle-sdk/data-type";
2+
import { defaultName } from "@giselle-sdk/giselle/react";
3+
import type { SuggestionProps } from "@tiptap/suggestion";
4+
import clsx from "clsx/lite";
5+
import {
6+
forwardRef,
7+
useCallback,
8+
useEffect,
9+
useImperativeHandle,
10+
useState,
11+
} from "react";
12+
13+
export interface SuggestionItem {
14+
id: string;
15+
node: GiselleNode;
16+
output: Output;
17+
label: string;
18+
}
19+
interface SuggestionListProps extends SuggestionProps<SuggestionItem> {}
20+
export interface SuggestionListRef {
21+
onKeyDown: (props: { event: KeyboardEvent }) => boolean;
22+
}
23+
24+
export const SuggestionList = forwardRef<
25+
SuggestionListRef,
26+
SuggestionListProps
27+
>(({ items, command }, ref) => {
28+
const [selectedIndex, setSelectedIndex] = useState(0);
29+
useEffect(() => {
30+
if (items.length >= 0) {
31+
setSelectedIndex(0);
32+
}
33+
}, [items.length]);
34+
const selectItem = useCallback(
35+
(index: number) => {
36+
const item = items[index];
37+
if (item) {
38+
command(item);
39+
}
40+
},
41+
[items, command],
42+
);
43+
44+
useImperativeHandle(ref, () => ({
45+
onKeyDown: ({ event }) => {
46+
if (items.length === 0) {
47+
return false;
48+
}
49+
50+
if (event.key === "ArrowUp") {
51+
setSelectedIndex((prev) => (prev + items.length - 1) % items.length);
52+
return true;
53+
}
54+
if (event.key === "ArrowDown") {
55+
setSelectedIndex((prev) => (prev + 1) % items.length);
56+
return true;
57+
}
58+
59+
if (event.key === "Enter") {
60+
selectItem(selectedIndex);
61+
return true;
62+
}
63+
64+
return false;
65+
},
66+
}));
67+
68+
if (items.length === 0) {
69+
return null;
70+
}
71+
72+
return (
73+
<div
74+
className={clsx(
75+
"rounded-[8px] bg-(image:--glass-bg)",
76+
"p-[4px] border border-glass-border/20 backdrop-blur-md shadow-xl",
77+
"after:absolute after:bg-(image:--glass-highlight-bg) after:left-4 after:right-4 after:h-px after:top-0",
78+
"w-fit",
79+
)}
80+
>
81+
{items.map(({ id, node, output: { label } }, index) => (
82+
<button
83+
type="button"
84+
key={id}
85+
onClick={() => selectItem(index)}
86+
className={clsx(
87+
"block w-full text-left px-[8px] py-[6px]",
88+
"text-[14px] text-text",
89+
"rounded-[4px]",
90+
"outline-none cursor-pointer",
91+
"transition-colors",
92+
selectedIndex === index
93+
? "bg-ghost-element-hover"
94+
: "hover:bg-ghost-element-hover/25",
95+
)}
96+
>
97+
{node.name ?? defaultName(node)} / {label}
98+
</button>
99+
))}
100+
</div>
101+
);
102+
});
103+
104+
SuggestionList.displayName = "SuggestionList";

0 commit comments

Comments
 (0)