@rebasepro/plugin-ai 0.0.1-canary.4829d6e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +81 -0
  3. package/dist/api.d.ts +34 -0
  4. package/dist/components/DataEnhancementControllerProvider.d.ts +14 -0
  5. package/dist/components/FormEnhanceAction.d.ts +3 -0
  6. package/dist/editor/useEditorAIController.d.ts +4 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.es.js +741 -0
  9. package/dist/index.es.js.map +1 -0
  10. package/dist/index.umd.js +772 -0
  11. package/dist/index.umd.js.map +1 -0
  12. package/dist/types/data_enhancement_controller.d.ts +71 -0
  13. package/dist/useDataEnhancementPlugin.d.ts +29 -0
  14. package/dist/utils/diffStrings.d.ts +7 -0
  15. package/dist/utils/properties.d.ts +3 -0
  16. package/dist/utils/strings_counter.d.ts +2 -0
  17. package/dist/utils/suggestions.d.ts +1 -0
  18. package/dist/utils/values.d.ts +1 -0
  19. package/package.json +99 -0
  20. package/src/api.ts +198 -0
  21. package/src/components/DataEnhancementControllerProvider.tsx +342 -0
  22. package/src/components/FormEnhanceAction.tsx +277 -0
  23. package/src/editor/useEditorAIController.tsx +37 -0
  24. package/src/index.ts +9 -0
  25. package/src/tests/diffStrings.test.ts +128 -0
  26. package/src/tests/strings_counter.test.ts +117 -0
  27. package/src/tests/suggestions.test.ts +53 -0
  28. package/src/tests/useDataEnhancementPlugin.test.tsx +51 -0
  29. package/src/tests/values.test.ts +87 -0
  30. package/src/types/data_enhancement_controller.tsx +75 -0
  31. package/src/useDataEnhancementPlugin.tsx +66 -0
  32. package/src/utils/diffStrings.ts +70 -0
  33. package/src/utils/properties.ts +168 -0
  34. package/src/utils/strings_counter.ts +22 -0
  35. package/src/utils/suggestions.ts +6 -0
  36. package/src/utils/values.ts +12 -0
  37. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,342 @@
1
+ import React, { PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ import {
4
+ DataEnhancementController,
5
+ EnhancedDataResult,
6
+ EnhanceParams,
7
+ InputProperty
8
+ } from "../types/data_enhancement_controller";
9
+ import {
10
+ useAuthController,
11
+ useCustomizationController,
12
+ useSnackbarController
13
+ } from "@rebasepro/app";
14
+ import { useUrlController } from "@rebasepro/admin";
15
+ import { DataDriver, Entity, CollectionConfig, PluginFormActionProps } from "@rebasepro/types";
16
+ import { enhanceDataAPIStream, fetchEntityPromptSuggestion } from "../api";
17
+ import { getAppendableSuggestion } from "../utils/suggestions";
18
+ import { getSimplifiedProperties } from "../utils/properties";
19
+ import { useEditorAIController } from "../editor/useEditorAIController";
20
+ import { getValueInPath } from "@rebasepro/utils";
21
+
22
+ const DataEnhancementControllerContext = React.createContext<DataEnhancementController>(null! as DataEnhancementController);
23
+
24
+ type DataEnhancementControllerProviderProps = {
25
+
26
+ apiKey: string;
27
+
28
+ getConfigForPath?: (props: {
29
+ path: string,
30
+ collection: CollectionConfig
31
+ }) => boolean;
32
+
33
+ host?: string;
34
+ }
35
+
36
+ export const useDataEnhancementController = (): DataEnhancementController => useContext(DataEnhancementControllerContext);
37
+
38
+ function getPropertyFromKey(properties: Record<string, InputProperty>, propertyKey: string) {
39
+ if (propertyKey in properties) {
40
+ return properties[propertyKey];
41
+ } else {
42
+ //split the property key
43
+ const split = propertyKey.split(".");
44
+ if (split.length === 1) {
45
+ return undefined;
46
+ }
47
+ const parentKey = split.slice(0, split.length - 1).join(".");
48
+ return getPropertyFromKey(properties, parentKey);
49
+
50
+ }
51
+ }
52
+
53
+ export function DataEnhancementControllerProvider({
54
+ apiKey,
55
+ getConfigForPath,
56
+ children,
57
+ host,
58
+ path,
59
+ collection,
60
+ formContext
61
+ }: PropsWithChildren<DataEnhancementControllerProviderProps & PluginFormActionProps>) {
62
+
63
+ const [enabled, setEnabled] = useState(false);
64
+ const [suggestions, setSuggestions] = useState<Record<string, string | number>>({});
65
+ const [loadingSuggestions, setLoadingSuggestions] = useState<string[]>([]);
66
+
67
+ const enhancingInProgress = useRef(false);
68
+
69
+ const authController = useAuthController();
70
+ const snackbarController = useSnackbarController();
71
+
72
+
73
+ const properties = useMemo(() => getSimplifiedProperties(collection.properties, formContext?.values ?? {}), [formContext?.values]);
74
+ // const preEnhanceValuesRef = React.useRef(formContext?.values ?? {});
75
+ const valuesRef = React.useRef(formContext?.values ?? {});
76
+ useEffect(() => {
77
+ if (!enhancingInProgress.current)
78
+ valuesRef.current = formContext?.values ?? {};
79
+ }, [formContext?.values]);
80
+
81
+ const allowReferenceDataSelection = false;
82
+
83
+ const updateConfig = useCallback(async () => {
84
+ if (!getConfigForPath) return;
85
+ const config = getConfigForPath({
86
+ path,
87
+ collection
88
+ });
89
+ if (config) {
90
+ setEnabled(true);
91
+ }
92
+ }, [collection, getConfigForPath, path]);
93
+
94
+ useEffect(() => {
95
+ if (!getConfigForPath) {
96
+ setEnabled(true);
97
+ } else {
98
+ updateConfig();
99
+ }
100
+
101
+ }, [getConfigForPath, updateConfig]);
102
+
103
+
104
+ const urlController = useUrlController();
105
+
106
+ const clearSuggestion = useCallback((propertyKey: string) => {
107
+ setSuggestions((prev) => {
108
+ //remove propertyKey from prev
109
+ const {
110
+ [propertyKey]: _,
111
+ ...rest
112
+ } = prev;
113
+ return rest;
114
+ });
115
+ }, []);
116
+
117
+ const appendValueDelta = useCallback((propertyKey: string, delta: string) => {
118
+
119
+ const property = getPropertyFromKey(properties, propertyKey);
120
+ if (delta === null || property?.disabled) {
121
+ return;
122
+ }
123
+
124
+ // clearSuggestion(propertyKey);
125
+ const value = getValueInPath(valuesRef.current, propertyKey);
126
+
127
+ const currentValue = value ? (value as string) + "" : "";
128
+ const updatedValue = currentValue + delta;
129
+ // if (currentValue.length === 0) updatedValue = updatedValue.trimStart();
130
+ valuesRef.current = {
131
+ ...valuesRef.current,
132
+ [propertyKey]: updatedValue
133
+ };
134
+ formContext?.setFieldValue(propertyKey, updatedValue, false);
135
+ setSuggestions(prev => ({
136
+ ...prev,
137
+ [propertyKey]: (prev[propertyKey] ?? "") + delta
138
+ }));
139
+ }, [properties, formContext]);
140
+
141
+ const updateSuggestedValues = useCallback((currentValues: object, updatedValues: Record<string, string | number>, replaceValues: boolean) => {
142
+
143
+ setLoadingSuggestions((prev) => {
144
+ return prev.filter(p => !Object.keys(updatedValues).includes(p));
145
+ });
146
+
147
+ Object.entries(updatedValues).forEach(([propertyKey, suggestion]) => {
148
+
149
+ const value = getValueInPath(currentValues, propertyKey);
150
+ const property = getPropertyFromKey(properties, propertyKey);
151
+
152
+ if (!property || suggestion === null || property?.disabled) {
153
+ return;
154
+ }
155
+
156
+ if (typeof suggestion === "number") {
157
+ formContext?.setFieldValue(propertyKey, suggestion);
158
+ return;
159
+ }
160
+
161
+ if (replaceValues) {
162
+ formContext?.setFieldValue(propertyKey, suggestion);
163
+ return;
164
+ }
165
+
166
+ const appendableValue = getAppendableSuggestion(suggestion, value);
167
+
168
+ const currentValue = value ? (value as string) + "" : "";
169
+ if (appendableValue) {
170
+ formContext?.setFieldValue(propertyKey, suggestion);
171
+ } else {
172
+ const multiline = property?.fieldConfigId === "multiline" || property?.fieldConfigId === "markdown";
173
+ const trimmedValue = currentValue.trimEnd();
174
+ if (multiline && (trimmedValue.endsWith(".") || trimmedValue.endsWith("?") || trimmedValue.endsWith("!") || trimmedValue.endsWith(":"))) {
175
+ formContext?.setFieldValue(propertyKey, trimmedValue + "\n\n" + (suggestion as string).trimStart());
176
+ } else {
177
+ formContext?.setFieldValue(propertyKey, trimmedValue + (trimmedValue.length > 0 ? " " : "") + (suggestion as string));
178
+ }
179
+ }
180
+ });
181
+
182
+ setSuggestions(prev => ({
183
+ ...prev,
184
+ ...Object.keys(updatedValues)
185
+ .reduce((acc, key) => {
186
+ const value = getValueInPath(formContext?.values, key);
187
+ const suggestion = updatedValues[key];
188
+ return {
189
+ ...acc,
190
+ [key]: getAppendableSuggestion(suggestion, value) ?? suggestion
191
+ };
192
+ }, {})
193
+ }));
194
+ }, [properties, formContext]);
195
+
196
+ const displayNeededSubscriptionSnackbar = useCallback((projectId: unknown) => {
197
+ snackbarController.open({
198
+ type: "warning",
199
+ message: "A valid subscription is needed in order to use this function.",
200
+ autoHideDuration: 4000
201
+ });
202
+ }, [snackbarController]);
203
+
204
+ const editorAIController = useEditorAIController({ getAuthToken: authController.getAuthToken });
205
+
206
+ const clearAllSuggestions = useCallback(() => {
207
+ setSuggestions({});
208
+ }, []);
209
+
210
+ const enhance = useCallback(async (props: EnhanceParams<Record<string, unknown>>): Promise<EnhancedDataResult | null> => {
211
+
212
+ if (!authController.user) {
213
+ snackbarController.open({
214
+ type: "warning",
215
+ message: "You need to be logged in to enhance data"
216
+ });
217
+ return Promise.reject(new Error("Not logged in"));
218
+ }
219
+
220
+ const resolvedPath = urlController.resolveDatabasePathsFrom(path);
221
+ const firebaseToken = await authController.getAuthToken();
222
+
223
+ if (props.propertyKey) {
224
+ clearSuggestion(props.propertyKey)
225
+ } else {
226
+ clearAllSuggestions();
227
+ }
228
+
229
+ setLoadingSuggestions((prev) => [...prev, ...(props.propertyKey ? [props.propertyKey] : Object.keys(properties))]);
230
+ enhancingInProgress.current = true;
231
+
232
+ const currentValues = valuesRef.current ?? {};
233
+
234
+ return new Promise((resolve, reject) => {
235
+ function onError(e: unknown) {
236
+ setLoadingSuggestions([]);
237
+ const err = e instanceof Error ? e : typeof e === "object" && e !== null ? e : new Error(String(e));
238
+ const errorObj = err as Record<string, unknown>;
239
+ if (errorObj.code === "payment-required") {
240
+ const data = errorObj.data as Record<string, unknown> | undefined;
241
+ const projectId = data?.projectId;
242
+ displayNeededSubscriptionSnackbar(projectId);
243
+ } else {
244
+ console.error("Enhance error", e);
245
+ }
246
+ reject(e);
247
+ enhancingInProgress.current = false;
248
+ }
249
+
250
+ try {
251
+ enhanceDataAPIStream({
252
+ ...props,
253
+ host,
254
+ apiKey,
255
+ properties,
256
+ path: resolvedPath,
257
+ entityName: collection.singularName ?? collection.name,
258
+ entityDescription: collection.description,
259
+
260
+ firebaseToken,
261
+ onUpdate: (suggestions) => {
262
+ console.debug("de onUpdate", suggestions);
263
+ updateSuggestedValues(currentValues, suggestions, props.replaceValues ?? false);
264
+ },
265
+ onUpdateDelta: (propertyKey: string, partialValue: string) => {
266
+ // console.debug("de delta", propertyKey, partialValue);
267
+ appendValueDelta(propertyKey, partialValue);
268
+ },
269
+ onError,
270
+ onEnd: (result) => {
271
+ console.debug("de onEnd", result);
272
+ if (result.errors) {
273
+ result.errors.forEach((error) => {
274
+ snackbarController.open({
275
+ type: "warning",
276
+ message: error
277
+ })
278
+ });
279
+ }
280
+ if (Object.keys(result.suggestions).length === 0) {
281
+ snackbarController.open({
282
+ type: "info",
283
+ autoHideDuration: 1800,
284
+ message: "No fields were updated"
285
+ })
286
+ }
287
+ setLoadingSuggestions([]);
288
+ resolve(result);
289
+ enhancingInProgress.current = false;
290
+ }
291
+ }).catch(onError);
292
+ } catch (e: unknown) {
293
+ onError(e);
294
+ }
295
+ });
296
+ }, [
297
+ authController, urlController, path, clearSuggestion, clearAllSuggestions,
298
+ properties, host, apiKey, collection, updateSuggestedValues, appendValueDelta, displayNeededSubscriptionSnackbar, snackbarController
299
+ ]);
300
+
301
+ const getSamplePrompts = useCallback(async (entityName: string, input?: string) => {
302
+ const firebaseToken = await authController.getAuthToken()
303
+ return fetchEntityPromptSuggestion({
304
+ host,
305
+ entityName,
306
+ firebaseToken,
307
+ apiKey,
308
+ input
309
+ });
310
+ }, [apiKey, authController.getAuthToken, host]);
311
+
312
+ const dataEnhancementController: DataEnhancementController = useMemo(() => ({
313
+ enabled,
314
+ suggestions,
315
+ clearSuggestion,
316
+ enhance,
317
+ allowReferenceDataSelection,
318
+ clearAllSuggestions,
319
+ getSamplePrompts,
320
+ loadingSuggestions,
321
+ editorAIController
322
+ }), [
323
+ enabled,
324
+ suggestions,
325
+ clearSuggestion,
326
+ enhance,
327
+ allowReferenceDataSelection,
328
+ clearAllSuggestions,
329
+ getSamplePrompts,
330
+ loadingSuggestions,
331
+ editorAIController
332
+ ]);
333
+
334
+ return (
335
+ <DataEnhancementControllerContext.Provider
336
+ value={dataEnhancementController}>
337
+ {children}
338
+ </DataEnhancementControllerContext.Provider>
339
+ );
340
+ }
341
+
342
+
@@ -0,0 +1,277 @@
1
+
2
+ import React, { useCallback, useDeferredValue, useEffect, useRef } from "react";
3
+
4
+ import {
5
+ Button,
6
+ CircularProgress,
7
+ cls,
8
+ focusedDisabled,
9
+ IconButton,
10
+ iconSize,
11
+ Menu,
12
+ MenuItem,
13
+ SendIcon,
14
+ Separator,
15
+ TextareaAutosize,
16
+ XIcon
17
+ } from "@rebasepro/ui";
18
+ import {
19
+ AIIcon,
20
+ useLargeLayout
21
+ } from "@rebasepro/app";
22
+ import { EntityStatus, PluginFormActionProps, Properties, Property } from "@rebasepro/types";
23
+ import { isPropertyBuilder, stripCollectionPath } from "@rebasepro/common";
24
+ import { useDataEnhancementController } from "./DataEnhancementControllerProvider";
25
+ import { SamplePrompt } from "../types/data_enhancement_controller";
26
+
27
+ export function FormEnhanceAction({
28
+ entityId,
29
+ path,
30
+ status,
31
+ collection,
32
+ formContext,
33
+ openEntityMode
34
+ }: PluginFormActionProps) {
35
+
36
+ const largeLayout = useLargeLayout();
37
+
38
+ const storageKey = createLocalStorageKey(path, status);
39
+
40
+ const [loading, setLoading] = React.useState(false);
41
+ const dataEnhancementController = useDataEnhancementController();
42
+
43
+ const [samplePrompts, setSamplePrompts] = React.useState<SamplePrompt[] | undefined>(undefined);
44
+ const [instructions, setInstructions] = React.useState<string>("");
45
+
46
+ const getSamplePrompts = dataEnhancementController?.getSamplePrompts;
47
+
48
+ const loadingPrompts = useRef(false);
49
+ const updateSuggestedPrompts = useCallback(async function updateSuggestedPrompts(instructions?: string) {
50
+ if (!getSamplePrompts) return;
51
+ if (loadingPrompts.current) return;
52
+ loadingPrompts.current = true;
53
+ const prompts = status === "new"
54
+ ? (await getSamplePrompts(collection.singularName ?? collection.name, instructions)).prompts
55
+ : getPromptsForExistingEntities(collection.properties);
56
+
57
+ const recentPromptsFromStorage = getRecentPromptsFromStorage(storageKey);
58
+ const recentPrompts = recentPromptsFromStorage.map(prompt => prompt.prompt);
59
+ setSamplePrompts([...recentPromptsFromStorage, ...prompts.filter(p => !recentPrompts.includes(p.prompt))].slice(0, 5));
60
+ loadingPrompts.current = false;
61
+ },
62
+ [collection.name, collection.singularName, getSamplePrompts, status]);
63
+
64
+ const deferredValues = useDeferredValue(formContext?.values);
65
+ // const enoughData = countStringCharacters(deferredValues, collection.properties) > 20;
66
+
67
+ useEffect(() => {
68
+ if (!dataEnhancementController) return;
69
+ if (!samplePrompts) {
70
+ setSamplePrompts(getRecentPromptsFromStorage(storageKey));
71
+ updateSuggestedPrompts().then();
72
+ }
73
+ }, [dataEnhancementController, samplePrompts, storageKey, updateSuggestedPrompts, instructions, status]);
74
+
75
+ useEffect(() => {
76
+ if (!dataEnhancementController) return;
77
+ updateSuggestedPrompts().then();
78
+ }, [dataEnhancementController, status]);
79
+
80
+ const enhance = (prompt?: string) => {
81
+ if (!dataEnhancementController || !formContext?.values) return;
82
+ setLoading(true);
83
+ if (prompt) {
84
+ addRecentPrompt(storageKey, prompt);
85
+ setSamplePrompts([{
86
+ prompt,
87
+ type: "recent"
88
+ }, ...(samplePrompts ?? []).slice(0, 5)]);
89
+ }
90
+ return dataEnhancementController.enhance({
91
+ entityId,
92
+ values: formContext!.values,
93
+ instructions: prompt,
94
+ replaceValues: true
95
+ }).finally(() => {
96
+ setLoading(false);
97
+ });
98
+ };
99
+
100
+ if (!dataEnhancementController?.enabled)
101
+ return null;
102
+
103
+ const suggestions = dataEnhancementController.suggestions;
104
+ const hasSuggestions = Object.values(suggestions).filter(Boolean).length > 0;
105
+
106
+ const disabledSuggestionActions = !hasSuggestions;
107
+ const promptSuggestionsEnabled = (samplePrompts ?? []).length > 0 && instructions.length === 0;
108
+
109
+ // const noIdSet = !formContext?.entityId;
110
+
111
+ function submit() {
112
+ enhance(instructions);
113
+ }
114
+
115
+ return (
116
+ <Menu
117
+ align={"end"}
118
+ sideOffset={8}
119
+ className={"max-w-[100vw]"}
120
+ trigger={<Button variant={"filled"}
121
+ color={"neutral"}
122
+ fullWidth={largeLayout && openEntityMode === "full_screen"}
123
+ size={"small"}
124
+ disabled={loading}>
125
+ {!loading && <AIIcon size={"small"}/>}
126
+ {loading && <CircularProgress size={"small"}/>}
127
+ Autofill
128
+ </Button>}>
129
+
130
+ <MenuItem className={"py-4"}
131
+ onClick={() => {
132
+ enhance();
133
+ }}>
134
+ <AIIcon size={"small"}/>
135
+ Autofill based on the current content
136
+ </MenuItem>
137
+
138
+ <Separator orientation={"horizontal"} className={"mt-2"}/>
139
+
140
+ {samplePrompts?.map((samplePrompt, index) => {
141
+ return <MenuItem
142
+ key={index + "_" + samplePrompt.prompt}
143
+ onClick={() => {
144
+ setInstructions(samplePrompt.prompt);
145
+ enhance(samplePrompt.prompt);
146
+ }}
147
+ >
148
+ <div className={"pl-9 grow text-text-secondary dark:text-text-secondary-dark"}>
149
+ {samplePrompt.prompt}
150
+ </div>
151
+
152
+ {samplePrompt.type === "recent" && <IconButton
153
+ onClick={(e) => {
154
+ e.preventDefault();
155
+ e.stopPropagation();
156
+ removeRecentPrompt(storageKey, samplePrompt.prompt);
157
+ setSamplePrompts((samplePrompts ?? []).filter(p => p.prompt !== samplePrompt.prompt));
158
+ }}
159
+ size={"smallest"}
160
+ >
161
+ <XIcon size={iconSize.smallest}/>
162
+ </IconButton>
163
+ }
164
+ </MenuItem>;
165
+ })}
166
+
167
+ <Separator orientation={"horizontal"}/>
168
+
169
+ <div
170
+ className={cls(
171
+ "my-2 w-[500px] max-w-full flex items-start text-surface-700 dark:text-surface-200"
172
+ )}>
173
+
174
+ <TextareaAutosize
175
+ className={cls("p-4 rounded-lg resize-none bg-surface-100 dark:bg-surface-950 mx-2 w-full grow outline-hidden max-h-[300px] overflow-auto", focusedDisabled)}
176
+ value={instructions}
177
+ autoFocus={status === "new"}
178
+ disabled={loading}
179
+ onFocus={(event) => {
180
+ event.stopPropagation();
181
+ }}
182
+ placeholder={"...or provide instructions"}
183
+ onKeyDown={(e) => {
184
+ e.stopPropagation();
185
+ if (e.key === "Enter" && !e.shiftKey) {
186
+ e.preventDefault();
187
+ submit();
188
+ }
189
+
190
+ }}
191
+ onChange={(e) => {
192
+ setInstructions(e.target.value);
193
+ }}
194
+ />
195
+
196
+ <IconButton
197
+ size={"small"}
198
+ onClick={() => {
199
+ setInstructions("");
200
+ }}
201
+ color={!instructions ? "primary" : undefined}
202
+ disabled={loading || !instructions}>
203
+ <XIcon size={iconSize.small}/>
204
+ </IconButton>
205
+
206
+ <IconButton
207
+ onClick={() => enhance(instructions)}
208
+ size={"small"}
209
+ color={!instructions ? "primary" : undefined}
210
+ disabled={loading || !instructions}>
211
+ {loading &&
212
+ <CircularProgress size={"smallest"}/>}
213
+ {!loading &&
214
+ <SendIcon color={"primary"}/>}
215
+ </IconButton>
216
+
217
+ </div>
218
+
219
+ </Menu>
220
+ );
221
+ }
222
+
223
+ function getPromptsForExistingEntities(properties: Properties): SamplePrompt[] {
224
+
225
+ const multilineProperties = Object.values(properties).filter((p: Property) => {
226
+ if (isPropertyBuilder(p)) {
227
+ return false;
228
+ }
229
+ return p.type === "string" && (p.ui?.markdown || p.ui?.multiline);
230
+ });
231
+
232
+ const multilinePrompt: Property | undefined = multilineProperties.length > 0
233
+ ? multilineProperties[Math.floor(Math.random() * multilineProperties.length)] as Property
234
+ : undefined;
235
+
236
+ const prompts = [
237
+ "Fill the missing fields",
238
+ "Translate the missing content"
239
+ ];
240
+ if (multilinePrompt) {
241
+ prompts.push(`Add 2 paragraphs to '${multilinePrompt.name}'`);
242
+ }
243
+ return prompts.map(p => ({
244
+ prompt: p,
245
+ type: "sample"
246
+ }));
247
+ }
248
+
249
+ const createLocalStorageKey = (path: string, status: EntityStatus) => {
250
+ const statusString = status === "new" ? "new" : "existing";
251
+ return `data_enhancement::${statusString}::${stripCollectionPath(path)}`;
252
+ };
253
+
254
+ const getRecentPromptsFromStorage = (storageKey: string): SamplePrompt[] => {
255
+ const item = localStorage.getItem(storageKey);
256
+ return item ? JSON.parse(item).map((e: string) => ({
257
+ prompt: e,
258
+ type: "recent"
259
+ })) : [];
260
+ };
261
+
262
+ const addRecentPrompt = (storageKey: string, prompt: string) => {
263
+ if (!prompt || prompt.trim().length === 0) {
264
+ return;
265
+ }
266
+ const recentPrompts = getRecentPromptsFromStorage(storageKey);
267
+ localStorage.setItem(storageKey, JSON.stringify([prompt, ...recentPrompts
268
+ .map(e => e.prompt)
269
+ .filter(e => e !== prompt)
270
+ .slice(0, 5)]));
271
+ };
272
+
273
+ const removeRecentPrompt = (storageKey: string, prompt: string) => {
274
+ localStorage.setItem(storageKey, JSON.stringify(getRecentPromptsFromStorage(storageKey)
275
+ .map(e => e.prompt)
276
+ .filter(e => e !== prompt)));
277
+ };
@@ -0,0 +1,37 @@
1
+ import { autocompleteStream } from "../api";
2
+ import { EditorAIController } from "@rebasepro/admin";
3
+
4
+ export function useEditorAIController({ getAuthToken }: { getAuthToken?: () => Promise<string> }): EditorAIController {
5
+ const autocomplete = async (textBefore: string, textAfter: string, onUpdate: (delta: string) => void) => {
6
+ if (!getAuthToken) {
7
+ throw new Error("Firebase token is required");
8
+ }
9
+ const firebaseToken = await getAuthToken();
10
+ return autocompleteStream({
11
+ firebaseToken,
12
+ textBefore,
13
+ textAfter,
14
+ onUpdate
15
+ });
16
+ }
17
+
18
+ return {
19
+ autocomplete
20
+ };
21
+ }
22
+
23
+ // async function * generateLoremIpsum(): AsyncGenerator<string> {
24
+ // const loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n# Heading\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
25
+ //
26
+ // const words = loremIpsum.split(" ");
27
+ //
28
+ // for (const word of words) {
29
+ // yield word;
30
+ // await new Promise(resolve => setTimeout(resolve, 100));
31
+ // }
32
+ // }
33
+ //
34
+ // const generator = generateLoremIpsum();
35
+ // for await (const word of generator) {
36
+ //
37
+ // }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export {
2
+ useDataEnhancementPlugin
3
+ } from "./useDataEnhancementPlugin";
4
+ export type {
5
+ DataEnhancementPluginProps
6
+ } from "./useDataEnhancementPlugin";
7
+
8
+
9
+ export * from "./editor/useEditorAIController";