Skip to content

Commit fb5e5f3

Browse files
committed
simpe autocomplete impl
1 parent 93d2d5e commit fb5e5f3

4 files changed

Lines changed: 89 additions & 23 deletions

File tree

examples/next-openai/src/app/components/vacation-notes.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ export function VacationNotes(): JSX.Element {
99
className="p-4"
1010
value={text}
1111
onChange={(value: string) => setText(value)}
12+
placeholder="What are your plans for your vacation?"
13+
autocompleteConfig={{
14+
autocomplete: (input: string) =>
15+
new Promise((resolve) => {
16+
setTimeout(() => {
17+
resolve(
18+
`You want to go to ${input}? That's a great place to visit!`
19+
);
20+
}, 3000);
21+
}),
22+
debounceTime: 1000,
23+
}}
1224
/>
1325
);
1426
}

packages/react-textarea/src/components/copilot-textarea/copilot-textarea.tsx

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import {
1515
withReact,
1616
} from "slate-react";
1717
import { HistoryEditor } from "slate-history";
18-
import { useCallback, useState } from "react";
18+
import { useCallback, useEffect, useRef, useState } from "react";
1919
import { Element } from "slate";
20+
import { editorToText } from "../../lib/editorToText";
21+
import { useAutocomplete } from "../../hooks/useAutocomplete";
2022

2123
export type CustomEditor = BaseEditor & ReactEditor & HistoryEditor;
2224

@@ -40,7 +42,7 @@ declare module "slate" {
4042
export interface AutocompleteConfig {
4143
autocomplete: (input: string) => Promise<string>;
4244
debounceTime?: number;
43-
};
45+
}
4446

4547
export interface CopilotTextareaProps {
4648
className?: string;
@@ -81,38 +83,24 @@ export function CopilotTextarea(props: CopilotTextareaProps): JSX.Element {
8183
const [editor] = useState(() => {
8284
const editor = withReact(createEditor());
8385
editor.onChange = () => {
84-
const suggestionAwareTextComponents: SuggestionAwareText[][] =
85-
editor.children.map((node) => {
86-
if (Element.isElement(node)) {
87-
return node.children.map((child) => {
88-
return child;
89-
});
90-
} else {
91-
return [node];
92-
}
93-
});
94-
95-
const flattened = suggestionAwareTextComponents.reduce(
96-
(acc, val) => acc.concat(val),
97-
[]
98-
);
99-
const text = flattened
100-
.map((textComponent) => textComponent.text)
101-
.join("\n");
102-
103-
props.onChange?.(text);
86+
props.onChange?.(editorToText(editor));
10487
};
10588
return editor;
10689
});
10790

91+
const handleAutocompleteKeyDown = useAutocomplete(
92+
editor,
93+
props.autocompleteConfig
94+
);
95+
10896
return (
10997
// Add the editable component inside the context.
11098
<Slate editor={editor} initialValue={initialValue}>
11199
<Editable
112100
className={props.className}
113101
renderElement={renderElement}
114102
renderLeaf={renderLeaf}
115-
onKeyDown={(event) => {}}
103+
onKeyDown={handleAutocompleteKeyDown}
116104
/>
117105
</Slate>
118106
);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useRef, useState } from "react";
2+
import {
3+
AutocompleteConfig,
4+
CustomEditor,
5+
} from "../components/copilot-textarea/copilot-textarea";
6+
import { Transforms } from "slate";
7+
import { editorToText } from "../lib/editorToText";
8+
9+
export function useAutocomplete(
10+
editor: CustomEditor,
11+
autocompleteConfig: AutocompleteConfig
12+
) {
13+
const [timer, setTimer] = useState<number | null>(null);
14+
const waitingForSuggestionRef = useRef(false);
15+
16+
const appendSuggestion = (suggestion: string) => {
17+
Transforms.insertText(editor, suggestion);
18+
};
19+
20+
const handleKeyDown = (event: React.KeyboardEvent) => {
21+
if (timer) clearTimeout(timer);
22+
23+
waitingForSuggestionRef.current = false;
24+
25+
setTimer(
26+
setTimeout(async () => {
27+
waitingForSuggestionRef.current = true;
28+
const suggestion = await autocompleteConfig.autocomplete(
29+
editorToText(editor)
30+
);
31+
if (waitingForSuggestionRef.current) {
32+
appendSuggestion(suggestion);
33+
waitingForSuggestionRef.current = false;
34+
}
35+
}, autocompleteConfig.debounceTime || 0)
36+
);
37+
};
38+
39+
return handleKeyDown;
40+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { BaseEditor, Element } from "slate";
2+
import { HistoryEditor } from "slate-history";
3+
import { ReactEditor } from "slate-react";
4+
import { SuggestionAwareText } from "../components/copilot-textarea/copilot-textarea";
5+
6+
export const editorToText = (
7+
editor: BaseEditor & ReactEditor & HistoryEditor
8+
) => {
9+
const suggestionAwareTextComponents: SuggestionAwareText[][] =
10+
editor.children.map((node) => {
11+
if (Element.isElement(node)) {
12+
return node.children.map((child) => {
13+
return child;
14+
});
15+
} else {
16+
return [node];
17+
}
18+
});
19+
20+
const flattened = suggestionAwareTextComponents.reduce(
21+
(acc, val) => acc.concat(val),
22+
[]
23+
);
24+
const text = flattened.map((textComponent) => textComponent.text).join("\n");
25+
return text;
26+
};

0 commit comments

Comments
 (0)