@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,741 @@
1
+ import React, { useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
2
+ import { AIIcon, useAuthController, useLargeLayout, useSnackbarController } from "@rebasepro/app";
3
+ import { getFieldId, useUrlController } from "@rebasepro/admin";
4
+ import { isPropertyBuilder, stripCollectionPath } from "@rebasepro/common";
5
+ import { getValueInPath } from "@rebasepro/utils";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { Button, CircularProgress, IconButton, Menu, MenuItem, SendIcon, Separator, TextareaAutosize, XIcon, cls, focusedDisabled, iconSize } from "@rebasepro/ui";
8
+ //#region src/utils/values.ts
9
+ function flatMapEntityValues(values, path = "") {
10
+ if (!values) return {};
11
+ return Object.entries(values).flatMap(([key, value]) => {
12
+ const currentPath = path ? `${path}.${key}` : key;
13
+ if (typeof value === "object") return flatMapEntityValues(value, currentPath);
14
+ else return { [currentPath]: value };
15
+ }).reduce((acc, curr) => ({
16
+ ...acc,
17
+ ...curr
18
+ }), {});
19
+ }
20
+ //#endregion
21
+ //#region src/api.ts
22
+ var DEFAULT_SERVER = "https://api.rebase.pro";
23
+ async function enhanceDataAPIStream(props) {
24
+ const flatValues = flatMapEntityValues(props.values);
25
+ const properties = props.properties;
26
+ const request = {
27
+ inputEntity: {
28
+ entityId: props.entityId,
29
+ values: flatValues
30
+ },
31
+ properties,
32
+ entityName: props.entityName,
33
+ entityDescription: props.entityDescription,
34
+ propertyKey: props.propertyKey,
35
+ propertyInstructions: props.propertyInstructions,
36
+ instructions: props.instructions
37
+ };
38
+ console.debug("enhanceDataAPIStream", request);
39
+ return fetch((props.host ?? DEFAULT_SERVER) + "/data/enhance_stream/", {
40
+ method: "POST",
41
+ headers: {
42
+ "Content-Type": "application/json",
43
+ Authorization: `Basic ${props.firebaseToken}`,
44
+ "x-de-api-key": `Basic ${props.apiKey}`
45
+ },
46
+ body: JSON.stringify(request)
47
+ }).then(async (res) => {
48
+ if (!res.ok) {
49
+ console.error("enhanceDataAPIStream error", res);
50
+ throw await res.json();
51
+ }
52
+ const reader = res.body?.getReader();
53
+ if (!reader) throw new Error("No reader");
54
+ for await (const chunk of readChunks(reader)) {
55
+ const str = new TextDecoder().decode(chunk);
56
+ try {
57
+ str.split("&$# ").forEach((s) => {
58
+ if (s && s.length > 0) {
59
+ const data = JSON.parse(s.trim());
60
+ if (data.type === "suggestion_delta") props.onUpdateDelta(data.data.propertyKey, data.data.partialValue);
61
+ else if (data.type === "suggestion") props.onUpdate(data.data);
62
+ else if (data.type === "result") props.onEnd(data.data);
63
+ }
64
+ });
65
+ } catch (e) {
66
+ console.error("str", str);
67
+ console.error("Error parsing stream", e);
68
+ props.onError(e instanceof Error ? e : new Error(String(e)));
69
+ }
70
+ }
71
+ });
72
+ }
73
+ function readChunks(reader) {
74
+ return { async *[Symbol.asyncIterator]() {
75
+ let readResult = await reader.read();
76
+ while (!readResult.done) {
77
+ yield readResult.value;
78
+ readResult = await reader.read();
79
+ }
80
+ } };
81
+ }
82
+ async function fetchEntityPromptSuggestion(props) {
83
+ return fetch((props.host ?? DEFAULT_SERVER) + "/data/prompt_autocomplete/", {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ Authorization: `Basic ${props.firebaseToken}`,
88
+ "x-de-api-key": `Basic ${props.apiKey}`
89
+ },
90
+ body: JSON.stringify({
91
+ entityName: props.entityName,
92
+ input: props.input ?? null
93
+ })
94
+ }).then(async (res) => {
95
+ const data = await res.json();
96
+ if (!res.ok) {
97
+ console.error("fetchEntityPromptSuggestion", data);
98
+ throw Error(data.message);
99
+ }
100
+ return { prompts: data.data.prompts.map((e) => ({
101
+ prompt: e,
102
+ type: "sample"
103
+ })) };
104
+ });
105
+ }
106
+ async function autocompleteStream(props) {
107
+ let result = "";
108
+ return fetch((props.host ?? DEFAULT_SERVER) + "/data/autocomplete/", {
109
+ method: "POST",
110
+ headers: {
111
+ "Content-Type": "application/json",
112
+ Authorization: `Basic ${props.firebaseToken}`
113
+ },
114
+ body: JSON.stringify({
115
+ textBefore: props.textBefore,
116
+ textAfter: props.textAfter
117
+ })
118
+ }).then(async (res) => {
119
+ if (!res.ok) {
120
+ console.error("enhanceDataAPIStream error", res);
121
+ throw await res.json();
122
+ }
123
+ const reader = res.body?.getReader();
124
+ if (!reader) throw new Error("No reader");
125
+ for await (const chunk of readChunks(reader)) {
126
+ const str = new TextDecoder().decode(chunk);
127
+ result += str;
128
+ console.debug("Autocomplete update:", str);
129
+ props.onUpdate(str);
130
+ }
131
+ }).then(() => {
132
+ console.debug("Autocomplete result:", result);
133
+ return result;
134
+ });
135
+ }
136
+ //#endregion
137
+ //#region src/utils/suggestions.ts
138
+ function getAppendableSuggestion(suggestion, value) {
139
+ const suggestionIncludesValue = typeof suggestion === "string" && typeof value === "string" && suggestion.toLowerCase().trim().startsWith(value.toLowerCase().trim());
140
+ return typeof value === "string" && suggestionIncludesValue ? suggestion.substring(suggestion.toLowerCase().trim().indexOf(value.toLowerCase().trim()) + value.trim().length) : void 0;
141
+ }
142
+ //#endregion
143
+ //#region src/utils/properties.ts
144
+ function getSimplifiedProperties(properties, values, path = "") {
145
+ if (!properties) return {};
146
+ return Object.entries(properties).map(([key, property]) => {
147
+ if (isPropertyBuilder(property)) return {};
148
+ const fullKey = path ? `${path}.${key}` : key;
149
+ return getSimplifiedProperty(property, fullKey, getValueInPath(values, fullKey));
150
+ }).reduce((a, b) => ({
151
+ ...a,
152
+ ...b
153
+ }), {});
154
+ }
155
+ function getSimpleProperty(property) {
156
+ const fieldId = getFieldId(property);
157
+ if (!fieldId) {
158
+ console.error("No fieldId found for property", property);
159
+ throw new Error("Field id not found");
160
+ }
161
+ return {
162
+ name: property.name,
163
+ description: property.description,
164
+ type: property.type,
165
+ fieldConfigId: fieldId,
166
+ enum: "enum" in property && property.enum ? getSimpleEnumValues(property.enum) : void 0,
167
+ disabled: Boolean(property.ui?.disabled || property.ui?.readOnly)
168
+ };
169
+ }
170
+ function getSimplifiedProperty(property, path, value) {
171
+ if (isPropertyBuilder(property)) return {};
172
+ if (property.type === "array") {
173
+ if (property.of && !Array.isArray(property.of) && !isPropertyBuilder(property.of)) {
174
+ const arrayParentProperty = {
175
+ name: property.name,
176
+ description: property.description,
177
+ type: property.type,
178
+ fieldConfigId: "repeat",
179
+ disabled: Boolean(property.ui?.disabled || property.ui?.readOnly),
180
+ of: getSimpleProperty(property.of)
181
+ };
182
+ return { [path]: arrayParentProperty };
183
+ } else if (property.oneOf) {
184
+ const arrayParentProperty = {
185
+ name: property.name,
186
+ description: property.description,
187
+ type: property.type,
188
+ fieldConfigId: "block",
189
+ disabled: Boolean(property.ui?.disabled || property.ui?.readOnly),
190
+ oneOf: {
191
+ typeField: property.oneOf.typeField,
192
+ valueField: property.oneOf.valueField,
193
+ properties: Object.entries(property.oneOf.properties).map(([key, prop]) => ({ [key]: getSimpleProperty(prop) })).reduce((a, b) => ({
194
+ ...a,
195
+ ...b
196
+ }), {})
197
+ }
198
+ };
199
+ if (!Array.isArray(value)) return { [path]: arrayParentProperty };
200
+ return value.map((v, i) => {
201
+ if (v == null) return {};
202
+ const typeKey = property.oneOf.typeField ?? "type";
203
+ const oneOfType = v[typeKey];
204
+ const valueKey = property.oneOf.valueField ?? "value";
205
+ const oneOfValue = v[valueKey];
206
+ const childProperty = property.oneOf.properties[oneOfType];
207
+ if (childProperty === void 0) {
208
+ console.error(`No property found for type ${oneOfType}`, property.oneOf.properties);
209
+ return {};
210
+ }
211
+ const simplifiedProperty = getSimplifiedProperty(childProperty, `${path}.${i}.${valueKey}`, oneOfValue);
212
+ return {
213
+ [`${path}.${i}.${typeKey}`]: oneOfType,
214
+ ...simplifiedProperty
215
+ };
216
+ }).reduce((a, b) => ({
217
+ ...a,
218
+ ...b
219
+ }), { [path]: arrayParentProperty });
220
+ }
221
+ } else if (property.type === "map") {
222
+ if (property.properties) {
223
+ const mapProperties = Object.entries(property.properties).map(([key, childProperty]) => {
224
+ return getSimplifiedProperty(childProperty, key, value && typeof value === "object" ? value[key] : void 0);
225
+ }).map((o) => attachPathToKeys(o, path)).reduce((a, b) => ({
226
+ ...a,
227
+ ...b
228
+ }), {});
229
+ if (Object.keys(mapProperties).length === 0) return {};
230
+ const mapParentProperty = {
231
+ name: property.name,
232
+ description: property.description,
233
+ type: property.type,
234
+ fieldConfigId: "group",
235
+ disabled: Boolean(property.ui?.disabled || property.ui?.readOnly)
236
+ };
237
+ return {
238
+ [path]: mapParentProperty,
239
+ ...mapProperties
240
+ };
241
+ }
242
+ } else {
243
+ if (!getFieldId(property)) {
244
+ console.warn(`No fieldId found for property ${path} with type ${property.type}`);
245
+ return {};
246
+ }
247
+ return { [path]: getSimpleProperty(property) };
248
+ }
249
+ return {};
250
+ }
251
+ function attachPathToKeys(obj, path = "") {
252
+ return Object.entries(obj).map(([key, value]) => {
253
+ return { [path ? `${path}.${key}` : key]: value };
254
+ }).reduce((a, b) => ({
255
+ ...a,
256
+ ...b
257
+ }), {});
258
+ }
259
+ function getSimpleEnumValues(enumValues) {
260
+ if (Array.isArray(enumValues)) return enumValues.map((v) => String(v.id));
261
+ if (typeof enumValues === "object") return Object.keys(enumValues);
262
+ throw Error("getSimpleEnumValues: Invalid enumValues");
263
+ }
264
+ //#endregion
265
+ //#region src/editor/useEditorAIController.tsx
266
+ function useEditorAIController({ getAuthToken }) {
267
+ const autocomplete = async (textBefore, textAfter, onUpdate) => {
268
+ if (!getAuthToken) throw new Error("Firebase token is required");
269
+ return autocompleteStream({
270
+ firebaseToken: await getAuthToken(),
271
+ textBefore,
272
+ textAfter,
273
+ onUpdate
274
+ });
275
+ };
276
+ return { autocomplete };
277
+ }
278
+ //#endregion
279
+ //#region src/components/DataEnhancementControllerProvider.tsx
280
+ var DataEnhancementControllerContext = React.createContext(null);
281
+ var useDataEnhancementController = () => useContext(DataEnhancementControllerContext);
282
+ function getPropertyFromKey(properties, propertyKey) {
283
+ if (propertyKey in properties) return properties[propertyKey];
284
+ else {
285
+ const split = propertyKey.split(".");
286
+ if (split.length === 1) return;
287
+ return getPropertyFromKey(properties, split.slice(0, split.length - 1).join("."));
288
+ }
289
+ }
290
+ function DataEnhancementControllerProvider({ apiKey, getConfigForPath, children, host, path, collection, formContext }) {
291
+ const [enabled, setEnabled] = useState(false);
292
+ const [suggestions, setSuggestions] = useState({});
293
+ const [loadingSuggestions, setLoadingSuggestions] = useState([]);
294
+ const enhancingInProgress = useRef(false);
295
+ const authController = useAuthController();
296
+ const snackbarController = useSnackbarController();
297
+ const properties = useMemo(() => getSimplifiedProperties(collection.properties, formContext?.values ?? {}), [formContext?.values]);
298
+ const valuesRef = React.useRef(formContext?.values ?? {});
299
+ useEffect(() => {
300
+ if (!enhancingInProgress.current) valuesRef.current = formContext?.values ?? {};
301
+ }, [formContext?.values]);
302
+ const allowReferenceDataSelection = false;
303
+ const updateConfig = useCallback(async () => {
304
+ if (!getConfigForPath) return;
305
+ if (getConfigForPath({
306
+ path,
307
+ collection
308
+ })) setEnabled(true);
309
+ }, [
310
+ collection,
311
+ getConfigForPath,
312
+ path
313
+ ]);
314
+ useEffect(() => {
315
+ if (!getConfigForPath) setEnabled(true);
316
+ else updateConfig();
317
+ }, [getConfigForPath, updateConfig]);
318
+ const urlController = useUrlController();
319
+ const clearSuggestion = useCallback((propertyKey) => {
320
+ setSuggestions((prev) => {
321
+ const { [propertyKey]: _, ...rest } = prev;
322
+ return rest;
323
+ });
324
+ }, []);
325
+ const appendValueDelta = useCallback((propertyKey, delta) => {
326
+ const property = getPropertyFromKey(properties, propertyKey);
327
+ if (delta === null || property?.disabled) return;
328
+ const value = getValueInPath(valuesRef.current, propertyKey);
329
+ const updatedValue = (value ? value + "" : "") + delta;
330
+ valuesRef.current = {
331
+ ...valuesRef.current,
332
+ [propertyKey]: updatedValue
333
+ };
334
+ formContext?.setFieldValue(propertyKey, updatedValue, false);
335
+ setSuggestions((prev) => ({
336
+ ...prev,
337
+ [propertyKey]: (prev[propertyKey] ?? "") + delta
338
+ }));
339
+ }, [properties, formContext]);
340
+ const updateSuggestedValues = useCallback((currentValues, updatedValues, replaceValues) => {
341
+ setLoadingSuggestions((prev) => {
342
+ return prev.filter((p) => !Object.keys(updatedValues).includes(p));
343
+ });
344
+ Object.entries(updatedValues).forEach(([propertyKey, suggestion]) => {
345
+ const value = getValueInPath(currentValues, propertyKey);
346
+ const property = getPropertyFromKey(properties, propertyKey);
347
+ if (!property || suggestion === null || property?.disabled) return;
348
+ if (typeof suggestion === "number") {
349
+ formContext?.setFieldValue(propertyKey, suggestion);
350
+ return;
351
+ }
352
+ if (replaceValues) {
353
+ formContext?.setFieldValue(propertyKey, suggestion);
354
+ return;
355
+ }
356
+ const appendableValue = getAppendableSuggestion(suggestion, value);
357
+ const currentValue = value ? value + "" : "";
358
+ if (appendableValue) formContext?.setFieldValue(propertyKey, suggestion);
359
+ else {
360
+ const multiline = property?.fieldConfigId === "multiline" || property?.fieldConfigId === "markdown";
361
+ const trimmedValue = currentValue.trimEnd();
362
+ if (multiline && (trimmedValue.endsWith(".") || trimmedValue.endsWith("?") || trimmedValue.endsWith("!") || trimmedValue.endsWith(":"))) formContext?.setFieldValue(propertyKey, trimmedValue + "\n\n" + suggestion.trimStart());
363
+ else formContext?.setFieldValue(propertyKey, trimmedValue + (trimmedValue.length > 0 ? " " : "") + suggestion);
364
+ }
365
+ });
366
+ setSuggestions((prev) => ({
367
+ ...prev,
368
+ ...Object.keys(updatedValues).reduce((acc, key) => {
369
+ const value = getValueInPath(formContext?.values, key);
370
+ const suggestion = updatedValues[key];
371
+ return {
372
+ ...acc,
373
+ [key]: getAppendableSuggestion(suggestion, value) ?? suggestion
374
+ };
375
+ }, {})
376
+ }));
377
+ }, [properties, formContext]);
378
+ const displayNeededSubscriptionSnackbar = useCallback((projectId) => {
379
+ snackbarController.open({
380
+ type: "warning",
381
+ message: "A valid subscription is needed in order to use this function.",
382
+ autoHideDuration: 4e3
383
+ });
384
+ }, [snackbarController]);
385
+ const editorAIController = useEditorAIController({ getAuthToken: authController.getAuthToken });
386
+ const clearAllSuggestions = useCallback(() => {
387
+ setSuggestions({});
388
+ }, []);
389
+ const enhance = useCallback(async (props) => {
390
+ if (!authController.user) {
391
+ snackbarController.open({
392
+ type: "warning",
393
+ message: "You need to be logged in to enhance data"
394
+ });
395
+ return Promise.reject(/* @__PURE__ */ new Error("Not logged in"));
396
+ }
397
+ const resolvedPath = urlController.resolveDatabasePathsFrom(path);
398
+ const firebaseToken = await authController.getAuthToken();
399
+ if (props.propertyKey) clearSuggestion(props.propertyKey);
400
+ else clearAllSuggestions();
401
+ setLoadingSuggestions((prev) => [...prev, ...props.propertyKey ? [props.propertyKey] : Object.keys(properties)]);
402
+ enhancingInProgress.current = true;
403
+ const currentValues = valuesRef.current ?? {};
404
+ return new Promise((resolve, reject) => {
405
+ function onError(e) {
406
+ setLoadingSuggestions([]);
407
+ const errorObj = e instanceof Error ? e : typeof e === "object" && e !== null ? e : new Error(String(e));
408
+ if (errorObj.code === "payment-required") {
409
+ const projectId = errorObj.data?.projectId;
410
+ displayNeededSubscriptionSnackbar(projectId);
411
+ } else console.error("Enhance error", e);
412
+ reject(e);
413
+ enhancingInProgress.current = false;
414
+ }
415
+ try {
416
+ enhanceDataAPIStream({
417
+ ...props,
418
+ host,
419
+ apiKey,
420
+ properties,
421
+ path: resolvedPath,
422
+ entityName: collection.singularName ?? collection.name,
423
+ entityDescription: collection.description,
424
+ firebaseToken,
425
+ onUpdate: (suggestions) => {
426
+ console.debug("de onUpdate", suggestions);
427
+ updateSuggestedValues(currentValues, suggestions, props.replaceValues ?? false);
428
+ },
429
+ onUpdateDelta: (propertyKey, partialValue) => {
430
+ appendValueDelta(propertyKey, partialValue);
431
+ },
432
+ onError,
433
+ onEnd: (result) => {
434
+ console.debug("de onEnd", result);
435
+ if (result.errors) result.errors.forEach((error) => {
436
+ snackbarController.open({
437
+ type: "warning",
438
+ message: error
439
+ });
440
+ });
441
+ if (Object.keys(result.suggestions).length === 0) snackbarController.open({
442
+ type: "info",
443
+ autoHideDuration: 1800,
444
+ message: "No fields were updated"
445
+ });
446
+ setLoadingSuggestions([]);
447
+ resolve(result);
448
+ enhancingInProgress.current = false;
449
+ }
450
+ }).catch(onError);
451
+ } catch (e) {
452
+ onError(e);
453
+ }
454
+ });
455
+ }, [
456
+ authController,
457
+ urlController,
458
+ path,
459
+ clearSuggestion,
460
+ clearAllSuggestions,
461
+ properties,
462
+ host,
463
+ apiKey,
464
+ collection,
465
+ updateSuggestedValues,
466
+ appendValueDelta,
467
+ displayNeededSubscriptionSnackbar,
468
+ snackbarController
469
+ ]);
470
+ const getSamplePrompts = useCallback(async (entityName, input) => {
471
+ return fetchEntityPromptSuggestion({
472
+ host,
473
+ entityName,
474
+ firebaseToken: await authController.getAuthToken(),
475
+ apiKey,
476
+ input
477
+ });
478
+ }, [
479
+ apiKey,
480
+ authController.getAuthToken,
481
+ host
482
+ ]);
483
+ const dataEnhancementController = useMemo(() => ({
484
+ enabled,
485
+ suggestions,
486
+ clearSuggestion,
487
+ enhance,
488
+ allowReferenceDataSelection,
489
+ clearAllSuggestions,
490
+ getSamplePrompts,
491
+ loadingSuggestions,
492
+ editorAIController
493
+ }), [
494
+ enabled,
495
+ suggestions,
496
+ clearSuggestion,
497
+ enhance,
498
+ allowReferenceDataSelection,
499
+ clearAllSuggestions,
500
+ getSamplePrompts,
501
+ loadingSuggestions,
502
+ editorAIController
503
+ ]);
504
+ return /* @__PURE__ */ jsx(DataEnhancementControllerContext.Provider, {
505
+ value: dataEnhancementController,
506
+ children
507
+ });
508
+ }
509
+ //#endregion
510
+ //#region src/components/FormEnhanceAction.tsx
511
+ function FormEnhanceAction({ entityId, path, status, collection, formContext, openEntityMode }) {
512
+ const largeLayout = useLargeLayout();
513
+ const storageKey = createLocalStorageKey(path, status);
514
+ const [loading, setLoading] = React.useState(false);
515
+ const dataEnhancementController = useDataEnhancementController();
516
+ const [samplePrompts, setSamplePrompts] = React.useState(void 0);
517
+ const [instructions, setInstructions] = React.useState("");
518
+ const getSamplePrompts = dataEnhancementController?.getSamplePrompts;
519
+ const loadingPrompts = useRef(false);
520
+ const updateSuggestedPrompts = useCallback(async function updateSuggestedPrompts(instructions) {
521
+ if (!getSamplePrompts) return;
522
+ if (loadingPrompts.current) return;
523
+ loadingPrompts.current = true;
524
+ const prompts = status === "new" ? (await getSamplePrompts(collection.singularName ?? collection.name, instructions)).prompts : getPromptsForExistingEntities(collection.properties);
525
+ const recentPromptsFromStorage = getRecentPromptsFromStorage(storageKey);
526
+ const recentPrompts = recentPromptsFromStorage.map((prompt) => prompt.prompt);
527
+ setSamplePrompts([...recentPromptsFromStorage, ...prompts.filter((p) => !recentPrompts.includes(p.prompt))].slice(0, 5));
528
+ loadingPrompts.current = false;
529
+ }, [
530
+ collection.name,
531
+ collection.singularName,
532
+ getSamplePrompts,
533
+ status
534
+ ]);
535
+ useDeferredValue(formContext?.values);
536
+ useEffect(() => {
537
+ if (!dataEnhancementController) return;
538
+ if (!samplePrompts) {
539
+ setSamplePrompts(getRecentPromptsFromStorage(storageKey));
540
+ updateSuggestedPrompts().then();
541
+ }
542
+ }, [
543
+ dataEnhancementController,
544
+ samplePrompts,
545
+ storageKey,
546
+ updateSuggestedPrompts,
547
+ instructions,
548
+ status
549
+ ]);
550
+ useEffect(() => {
551
+ if (!dataEnhancementController) return;
552
+ updateSuggestedPrompts().then();
553
+ }, [dataEnhancementController, status]);
554
+ const enhance = (prompt) => {
555
+ if (!dataEnhancementController || !formContext?.values) return;
556
+ setLoading(true);
557
+ if (prompt) {
558
+ addRecentPrompt(storageKey, prompt);
559
+ setSamplePrompts([{
560
+ prompt,
561
+ type: "recent"
562
+ }, ...(samplePrompts ?? []).slice(0, 5)]);
563
+ }
564
+ return dataEnhancementController.enhance({
565
+ entityId,
566
+ values: formContext.values,
567
+ instructions: prompt,
568
+ replaceValues: true
569
+ }).finally(() => {
570
+ setLoading(false);
571
+ });
572
+ };
573
+ if (!dataEnhancementController?.enabled) return null;
574
+ const suggestions = dataEnhancementController.suggestions;
575
+ Object.values(suggestions).filter(Boolean).length;
576
+ (samplePrompts ?? []).length > 0 && instructions.length;
577
+ function submit() {
578
+ enhance(instructions);
579
+ }
580
+ return /* @__PURE__ */ jsxs(Menu, {
581
+ align: "end",
582
+ sideOffset: 8,
583
+ className: "max-w-[100vw]",
584
+ trigger: /* @__PURE__ */ jsxs(Button, {
585
+ variant: "filled",
586
+ color: "neutral",
587
+ fullWidth: largeLayout && openEntityMode === "full_screen",
588
+ size: "small",
589
+ disabled: loading,
590
+ children: [
591
+ !loading && /* @__PURE__ */ jsx(AIIcon, { size: "small" }),
592
+ loading && /* @__PURE__ */ jsx(CircularProgress, { size: "small" }),
593
+ "Autofill"
594
+ ]
595
+ }),
596
+ children: [
597
+ /* @__PURE__ */ jsxs(MenuItem, {
598
+ className: "py-4",
599
+ onClick: () => {
600
+ enhance();
601
+ },
602
+ children: [/* @__PURE__ */ jsx(AIIcon, { size: "small" }), "Autofill based on the current content"]
603
+ }),
604
+ /* @__PURE__ */ jsx(Separator, {
605
+ orientation: "horizontal",
606
+ className: "mt-2"
607
+ }),
608
+ samplePrompts?.map((samplePrompt, index) => {
609
+ return /* @__PURE__ */ jsxs(MenuItem, {
610
+ onClick: () => {
611
+ setInstructions(samplePrompt.prompt);
612
+ enhance(samplePrompt.prompt);
613
+ },
614
+ children: [/* @__PURE__ */ jsx("div", {
615
+ className: "pl-9 grow text-text-secondary dark:text-text-secondary-dark",
616
+ children: samplePrompt.prompt
617
+ }), samplePrompt.type === "recent" && /* @__PURE__ */ jsx(IconButton, {
618
+ onClick: (e) => {
619
+ e.preventDefault();
620
+ e.stopPropagation();
621
+ removeRecentPrompt(storageKey, samplePrompt.prompt);
622
+ setSamplePrompts((samplePrompts ?? []).filter((p) => p.prompt !== samplePrompt.prompt));
623
+ },
624
+ size: "smallest",
625
+ children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
626
+ })]
627
+ }, index + "_" + samplePrompt.prompt);
628
+ }),
629
+ /* @__PURE__ */ jsx(Separator, { orientation: "horizontal" }),
630
+ /* @__PURE__ */ jsxs("div", {
631
+ className: cls("my-2 w-[500px] max-w-full flex items-start text-surface-700 dark:text-surface-200"),
632
+ children: [
633
+ /* @__PURE__ */ jsx(TextareaAutosize, {
634
+ 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),
635
+ value: instructions,
636
+ autoFocus: status === "new",
637
+ disabled: loading,
638
+ onFocus: (event) => {
639
+ event.stopPropagation();
640
+ },
641
+ placeholder: "...or provide instructions",
642
+ onKeyDown: (e) => {
643
+ e.stopPropagation();
644
+ if (e.key === "Enter" && !e.shiftKey) {
645
+ e.preventDefault();
646
+ submit();
647
+ }
648
+ },
649
+ onChange: (e) => {
650
+ setInstructions(e.target.value);
651
+ }
652
+ }),
653
+ /* @__PURE__ */ jsx(IconButton, {
654
+ size: "small",
655
+ onClick: () => {
656
+ setInstructions("");
657
+ },
658
+ color: !instructions ? "primary" : void 0,
659
+ disabled: loading || !instructions,
660
+ children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.small })
661
+ }),
662
+ /* @__PURE__ */ jsxs(IconButton, {
663
+ onClick: () => enhance(instructions),
664
+ size: "small",
665
+ color: !instructions ? "primary" : void 0,
666
+ disabled: loading || !instructions,
667
+ children: [loading && /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }), !loading && /* @__PURE__ */ jsx(SendIcon, { color: "primary" })]
668
+ })
669
+ ]
670
+ })
671
+ ]
672
+ });
673
+ }
674
+ function getPromptsForExistingEntities(properties) {
675
+ const multilineProperties = Object.values(properties).filter((p) => {
676
+ if (isPropertyBuilder(p)) return false;
677
+ return p.type === "string" && (p.ui?.markdown || p.ui?.multiline);
678
+ });
679
+ const multilinePrompt = multilineProperties.length > 0 ? multilineProperties[Math.floor(Math.random() * multilineProperties.length)] : void 0;
680
+ const prompts = ["Fill the missing fields", "Translate the missing content"];
681
+ if (multilinePrompt) prompts.push(`Add 2 paragraphs to '${multilinePrompt.name}'`);
682
+ return prompts.map((p) => ({
683
+ prompt: p,
684
+ type: "sample"
685
+ }));
686
+ }
687
+ var createLocalStorageKey = (path, status) => {
688
+ return `data_enhancement::${status === "new" ? "new" : "existing"}::${stripCollectionPath(path)}`;
689
+ };
690
+ var getRecentPromptsFromStorage = (storageKey) => {
691
+ const item = localStorage.getItem(storageKey);
692
+ return item ? JSON.parse(item).map((e) => ({
693
+ prompt: e,
694
+ type: "recent"
695
+ })) : [];
696
+ };
697
+ var addRecentPrompt = (storageKey, prompt) => {
698
+ if (!prompt || prompt.trim().length === 0) return;
699
+ const recentPrompts = getRecentPromptsFromStorage(storageKey);
700
+ localStorage.setItem(storageKey, JSON.stringify([prompt, ...recentPrompts.map((e) => e.prompt).filter((e) => e !== prompt).slice(0, 5)]));
701
+ };
702
+ var removeRecentPrompt = (storageKey, prompt) => {
703
+ localStorage.setItem(storageKey, JSON.stringify(getRecentPromptsFromStorage(storageKey).map((e) => e.prompt).filter((e) => e !== prompt)));
704
+ };
705
+ //#endregion
706
+ //#region src/useDataEnhancementPlugin.tsx
707
+ var DEFAULT_API_KEY = "fcms-U9jdDii0xXWSDC34asfrf54lbkFJBfKfRWcEDEwdc4V5wDWEDF";
708
+ /**
709
+ * Use this hook to initialise the data enhancement plugin.
710
+ * This is likely the only hook you will need to use.
711
+ * @param props
712
+ */
713
+ function useDataEnhancementPlugin(props) {
714
+ const apiKey = props?.apiKey ?? DEFAULT_API_KEY;
715
+ const getConfigForPath = props?.getConfigForPath;
716
+ return React.useMemo(() => ({
717
+ key: "data_enhancement",
718
+ slots: [{
719
+ slot: "form.actions",
720
+ Component: FormEnhanceAction,
721
+ order: 40
722
+ }],
723
+ providers: [{
724
+ scope: "form",
725
+ Component: DataEnhancementControllerProvider,
726
+ props: {
727
+ apiKey,
728
+ getConfigForPath,
729
+ host: props?.host
730
+ }
731
+ }]
732
+ }), [
733
+ apiKey,
734
+ getConfigForPath,
735
+ props?.host
736
+ ]);
737
+ }
738
+ //#endregion
739
+ export { useDataEnhancementPlugin, useEditorAIController };
740
+
741
+ //# sourceMappingURL=index.es.js.map