@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.
- package/LICENSE +22 -0
- package/README.md +81 -0
- package/dist/api.d.ts +34 -0
- package/dist/components/DataEnhancementControllerProvider.d.ts +14 -0
- package/dist/components/FormEnhanceAction.d.ts +3 -0
- package/dist/editor/useEditorAIController.d.ts +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.es.js +741 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +772 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/types/data_enhancement_controller.d.ts +71 -0
- package/dist/useDataEnhancementPlugin.d.ts +29 -0
- package/dist/utils/diffStrings.d.ts +7 -0
- package/dist/utils/properties.d.ts +3 -0
- package/dist/utils/strings_counter.d.ts +2 -0
- package/dist/utils/suggestions.d.ts +1 -0
- package/dist/utils/values.d.ts +1 -0
- package/package.json +99 -0
- package/src/api.ts +198 -0
- package/src/components/DataEnhancementControllerProvider.tsx +342 -0
- package/src/components/FormEnhanceAction.tsx +277 -0
- package/src/editor/useEditorAIController.tsx +37 -0
- package/src/index.ts +9 -0
- package/src/tests/diffStrings.test.ts +128 -0
- package/src/tests/strings_counter.test.ts +117 -0
- package/src/tests/suggestions.test.ts +53 -0
- package/src/tests/useDataEnhancementPlugin.test.tsx +51 -0
- package/src/tests/values.test.ts +87 -0
- package/src/types/data_enhancement_controller.tsx +75 -0
- package/src/useDataEnhancementPlugin.tsx +66 -0
- package/src/utils/diffStrings.ts +70 -0
- package/src/utils/properties.ts +168 -0
- package/src/utils/strings_counter.ts +22 -0
- package/src/utils/suggestions.ts +6 -0
- package/src/utils/values.ts +12 -0
- 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
|
+
// }
|