@rebasepro/plugin-ai 0.12.1-canary.gf4240e3 → 0.13.0
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/dist/api.d.ts +2 -0
- package/dist/index.es.js +30 -32
- package/dist/index.es.js.map +1 -1
- package/package.json +9 -8
- package/src/api.ts +3 -0
- package/src/components/AutofillReviewDialog.tsx +8 -2
- package/src/components/DataEnhancementControllerProvider.tsx +17 -8
- package/src/components/FormEnhanceAction.tsx +58 -32
- package/src/tests/AutofillReviewDialog.test.tsx +340 -0
- package/src/tests/properties.test.ts +420 -0
- package/src/tests/review.test.tsx +133 -0
- package/src/tests/useEditorAIController.test.ts +98 -0
package/dist/api.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export declare function autocompleteStream(props: {
|
|
|
54
54
|
*/
|
|
55
55
|
export declare function fetchPromptSuggestions(props: {
|
|
56
56
|
entityName: string;
|
|
57
|
+
/** Ties suggestions to the domain — see the note on the service's side. */
|
|
58
|
+
entityDescription?: string;
|
|
57
59
|
input?: string;
|
|
58
60
|
endpoint?: string;
|
|
59
61
|
signal?: AbortSignal;
|
package/dist/index.es.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getFieldId } from "@rebasepro/admin";
|
|
|
3
3
|
import { isPropertyBuilder, stripCollectionPath } from "@rebasepro/common";
|
|
4
4
|
import { getValueInPath } from "@rebasepro/utils";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Menu, MenuItem, SendIcon, Separator, TextareaAutosize, Typography, XIcon, cls, focusedDisabled, iconSize } from "@rebasepro/ui";
|
|
6
|
+
import { Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Menu, MenuItem, SendIcon, Separator, TextareaAutosize, Typography, XIcon, cls, fieldBackgroundMixin, focusedDisabled, iconSize } from "@rebasepro/ui";
|
|
7
7
|
import { AIIcon } from "@rebasepro/app";
|
|
8
8
|
/**
|
|
9
9
|
* ## No credentials cross this boundary
|
|
@@ -175,6 +175,7 @@ async function fetchPromptSuggestions(props) {
|
|
|
175
175
|
headers: { "Content-Type": "application/json" },
|
|
176
176
|
body: JSON.stringify({
|
|
177
177
|
entityName: props.entityName,
|
|
178
|
+
entityDescription: props.entityDescription,
|
|
178
179
|
input: props.input
|
|
179
180
|
}),
|
|
180
181
|
signal: props.signal
|
|
@@ -479,10 +480,7 @@ function DataEnhancementControllerProvider({ getConfigForPath, children, endpoin
|
|
|
479
480
|
setReview((current) => current && {
|
|
480
481
|
...current,
|
|
481
482
|
status: "ready",
|
|
482
|
-
fields: current.fields.
|
|
483
|
-
...f,
|
|
484
|
-
pending: false
|
|
485
|
-
}))
|
|
483
|
+
fields: current.fields.filter((f) => !f.pending)
|
|
486
484
|
});
|
|
487
485
|
} catch (e) {
|
|
488
486
|
const message = e instanceof Error ? e.message : "Autofill could not be completed";
|
|
@@ -490,10 +488,7 @@ function DataEnhancementControllerProvider({ getConfigForPath, children, endpoin
|
|
|
490
488
|
...current,
|
|
491
489
|
status: "failed",
|
|
492
490
|
error: message,
|
|
493
|
-
fields: current.fields.
|
|
494
|
-
...f,
|
|
495
|
-
pending: false
|
|
496
|
-
}))
|
|
491
|
+
fields: current.fields.filter((f) => !f.pending)
|
|
497
492
|
});
|
|
498
493
|
}
|
|
499
494
|
}, [
|
|
@@ -535,8 +530,9 @@ function DataEnhancementControllerProvider({ getConfigForPath, children, endpoin
|
|
|
535
530
|
const getSamplePrompts = useCallback((entityName, input) => fetchPromptSuggestions({
|
|
536
531
|
endpoint,
|
|
537
532
|
entityName,
|
|
533
|
+
entityDescription: collection.description,
|
|
538
534
|
input
|
|
539
|
-
}), [endpoint]);
|
|
535
|
+
}), [endpoint, collection.description]);
|
|
540
536
|
const dataEnhancementController = useMemo(() => ({
|
|
541
537
|
enabled,
|
|
542
538
|
review,
|
|
@@ -619,6 +615,7 @@ function AutofillReviewDialog() {
|
|
|
619
615
|
onCheckedChange: () => controller.toggleAll(!allSelected)
|
|
620
616
|
}), /* @__PURE__ */ jsx(Typography, {
|
|
621
617
|
variant: "label",
|
|
618
|
+
component: "span",
|
|
622
619
|
color: "secondary",
|
|
623
620
|
children: allSelected ? "Deselect all" : "Select all"
|
|
624
621
|
})]
|
|
@@ -687,6 +684,7 @@ function ProposedFieldRow({ field, onToggle }) {
|
|
|
687
684
|
children: [
|
|
688
685
|
/* @__PURE__ */ jsx(Typography, {
|
|
689
686
|
variant: "label",
|
|
687
|
+
component: "span",
|
|
690
688
|
children: field.label
|
|
691
689
|
}),
|
|
692
690
|
replaces && /* @__PURE__ */ jsx(Typography, {
|
|
@@ -855,10 +853,11 @@ function FormEnhanceAction({ path, status, collection, formContext }) {
|
|
|
855
853
|
}),
|
|
856
854
|
/* @__PURE__ */ jsx(Separator, { orientation: "horizontal" }),
|
|
857
855
|
/* @__PURE__ */ jsxs("div", {
|
|
858
|
-
className: cls("my-2 w-[500px] max-w-full flex items-
|
|
859
|
-
children: [
|
|
860
|
-
|
|
861
|
-
|
|
856
|
+
className: cls("my-2 px-4 py-2 gap-4 w-[500px] max-w-full flex items-center text-surface-700 dark:text-surface-200"),
|
|
857
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
858
|
+
className: "relative w-full grow",
|
|
859
|
+
children: [/* @__PURE__ */ jsx(TextareaAutosize, {
|
|
860
|
+
className: cls("p-3 pr-12 rounded-lg resize-none w-full outline-hidden max-h-[300px] overflow-auto", fieldBackgroundMixin, focusedDisabled),
|
|
862
861
|
value: instructions,
|
|
863
862
|
autoFocus: status === "new",
|
|
864
863
|
disabled: loading,
|
|
@@ -876,24 +875,23 @@ function FormEnhanceAction({ path, status, collection, formContext }) {
|
|
|
876
875
|
onChange: (e) => {
|
|
877
876
|
setInstructions(e.target.value);
|
|
878
877
|
}
|
|
879
|
-
}),
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
})
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
]
|
|
878
|
+
}), instructions.length > 0 && !loading && /* @__PURE__ */ jsx("div", {
|
|
879
|
+
className: "flex flex-row justify-center items-center absolute h-full right-0 top-0 mr-2",
|
|
880
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
881
|
+
size: "small",
|
|
882
|
+
onClick: () => {
|
|
883
|
+
setInstructions("");
|
|
884
|
+
},
|
|
885
|
+
children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.small })
|
|
886
|
+
})
|
|
887
|
+
})]
|
|
888
|
+
}), /* @__PURE__ */ jsxs(IconButton, {
|
|
889
|
+
onClick: () => generate(instructions),
|
|
890
|
+
size: "small",
|
|
891
|
+
color: !instructions ? "primary" : void 0,
|
|
892
|
+
disabled: loading || !instructions,
|
|
893
|
+
children: [loading && /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }), !loading && /* @__PURE__ */ jsx(SendIcon, { size: iconSize.small })]
|
|
894
|
+
})]
|
|
897
895
|
})
|
|
898
896
|
]
|
|
899
897
|
}), /* @__PURE__ */ jsx(AutofillReviewDialog, {})] });
|
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","names":[],"sources":["../src/api.ts","../src/utils/properties.ts","../src/utils/values.ts","../src/editor/useEditorAIController.tsx","../src/components/DataEnhancementControllerProvider.tsx","../src/components/AutofillReviewDialog.tsx","../src/components/FormEnhanceAction.tsx","../src/useDataEnhancementPlugin.tsx"],"sourcesContent":["import {\n AutofillRequest,\n AutofillResult,\n AiStatus,\n SamplePromptsResult\n} from \"./types/data_enhancement_controller\";\n\n/**\n * The hosted service Rebase runs for this plugin.\n *\n * The previous value here was `https://api.rebase.pro`, a FireCMS-era host that\n * resolves but serves nothing — every path 404s — so Autofill had never worked\n * in a Rebase install. This one is served by the control plane\n * (`saas/backend/functions/ai.ts`). Point `endpoint` somewhere else to run your\n * own; the wire format below is the whole contract.\n */\nexport const DEFAULT_AI_ENDPOINT = \"https://app.rebase.pro/api/functions/ai\";\n\n/**\n * ## No credentials cross this boundary\n *\n * The old client sent the tenant's Rebase JWT as `Authorization: Basic <jwt>`\n * plus a hardcoded `fcms-…` key compiled into the published package. Both were\n * wrong in the same way: a self-hosted backend signs its tokens with its own\n * secret, so no external service can verify one — sending it only handed a live\n * credential to a third party that had no use for it.\n *\n * These requests are anonymous. The service bounds cost by rate limit and daily\n * ceiling rather than by identity, and reports through {@link fetchAiStatus}\n * when it can no longer serve — which is what keeps the UI from offering an\n * action that is going to fail.\n */\nfunction endpointOf(endpoint: string | undefined, path: string): string {\n return (endpoint ?? DEFAULT_AI_ENDPOINT).replace(/\\/+$/, \"\") + path;\n}\n\n/** One `event:`/`data:` pair off the wire. */\ntype ServerSentEvent = { event: string; data: string };\n\n/** Not global: `exec` must not carry `lastIndex` between buffer reads. */\nconst SSE_SEPARATOR = /\\r?\\n\\r?\\n/;\n\n/**\n * Parse an SSE body incrementally.\n *\n * The framing this replaces split each chunk on the literal `\"&$# \"` and\n * `JSON.parse`d the pieces, which corrupted itself the moment a delimiter\n * straddled two reads — and network reads land wherever they land. Buffering\n * until a blank line is the fix, and it is also just what SSE specifies.\n */\nasync function* readServerSentEvents(response: Response): AsyncGenerator<ServerSentEvent> {\n const reader = response.body?.getReader();\n if (!reader) throw new Error(\"The AI service returned no response body\");\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n // A record ends at a blank line. `\\r\\n` is tolerated because proxies\n // rewrite line endings. The separator is located with `exec` rather\n // than `search` so its actual length is known — a `\\r\\n\\r\\n` boundary\n // is four characters, not two, and slicing by the wrong count leaves a\n // stray newline that swallows the next record's `event:` field.\n let match = SSE_SEPARATOR.exec(buffer);\n while (match) {\n const raw = buffer.slice(0, match.index);\n buffer = buffer.slice(match.index + match[0].length);\n const parsed = parseEventBlock(raw);\n if (parsed) yield parsed;\n match = SSE_SEPARATOR.exec(buffer);\n }\n }\n}\n\nfunction parseEventBlock(block: string): ServerSentEvent | undefined {\n let event = \"message\";\n const dataLines: string[] = [];\n for (const line of block.split(/\\r?\\n/)) {\n if (line.startsWith(\":\")) continue; // comment / keep-alive\n const separator = line.indexOf(\":\");\n const field = separator === -1 ? line : line.slice(0, separator);\n const rawValue = separator === -1 ? \"\" : line.slice(separator + 1);\n const value = rawValue.startsWith(\" \") ? rawValue.slice(1) : rawValue;\n if (field === \"event\") event = value;\n else if (field === \"data\") dataLines.push(value);\n }\n if (dataLines.length === 0) return undefined;\n return { event,\ndata: dataLines.join(\"\\n\") };\n}\n\n/** Pull a message out of the control plane's `{ error: { message } }` envelope. */\nasync function errorFrom(response: Response, fallback: string): Promise<Error> {\n try {\n const body = await response.json();\n const message = body?.error?.message;\n if (typeof message === \"string\" && message) return new Error(message);\n } catch {\n /* not JSON — fall through */\n }\n return new Error(fallback);\n}\n\n/**\n * Ask the service whether it can serve a request at all.\n *\n * The plugin gates every affordance on this. A missing provider key, an\n * exhausted daily quota or an unreachable host all resolve to `available:\n * false`, and the Autofill button is simply not rendered — rather than\n * rendered, clicked, and failed.\n */\nexport async function fetchAiStatus(props: { endpoint?: string; signal?: AbortSignal }): Promise<AiStatus> {\n const response = await fetch(endpointOf(props.endpoint, \"/status\"), {\n method: \"GET\",\n signal: props.signal\n });\n if (!response.ok) return { available: false };\n const body = await response.json();\n return {\n available: Boolean(body?.available),\n model: typeof body?.model === \"string\" ? body.model : undefined,\n features: Array.isArray(body?.features) ? body.features : undefined\n };\n}\n\n/**\n * Fill a record, streaming each field as the service writes it.\n *\n * `onDelta` fires with more text for a field still being written; `onValue`\n * fires once a field is complete and carries its final, correctly typed value.\n * A caller that implements only `onValue` still ends up with the right record —\n * the deltas exist so a long text field fills in visibly instead of appearing\n * all at once.\n */\nexport async function autofillStream(props: {\n request: AutofillRequest;\n endpoint?: string;\n signal?: AbortSignal;\n onDelta: (key: string, text: string) => void;\n onValue: (key: string, value: unknown) => void;\n}): Promise<AutofillResult> {\n const response = await fetch(endpointOf(props.endpoint, \"/autofill\"), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(props.request),\n signal: props.signal\n });\n\n if (!response.ok) {\n throw await errorFrom(response, \"The AI service could not complete this request.\");\n }\n\n let result: AutofillResult = { suggestions: {} };\n\n for await (const { event, data } of readServerSentEvents(response)) {\n let payload: any;\n try {\n payload = JSON.parse(data);\n } catch {\n // One malformed record must not abort a stream that is otherwise\n // delivering good fields.\n continue;\n }\n\n if (event === \"suggestion_delta\") {\n props.onDelta(payload.key, payload.text);\n } else if (event === \"suggestion\") {\n props.onValue(payload.key, payload.value);\n } else if (event === \"done\") {\n result = {\n suggestions: payload.suggestions ?? {},\n usage: payload.usage\n };\n } else if (event === \"error\") {\n throw new Error(payload.message ?? \"The AI service reported an error.\");\n }\n }\n\n return result;\n}\n\n/** Inline continuation for the rich-text editor. Streams plain text. */\nexport async function autocompleteStream(props: {\n textBefore?: string;\n textAfter?: string;\n endpoint?: string;\n signal?: AbortSignal;\n onDelta: (text: string) => void;\n}): Promise<string> {\n const response = await fetch(endpointOf(props.endpoint, \"/autocomplete\"), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n textBefore: props.textBefore ?? \"\",\n textAfter: props.textAfter ?? \"\"\n }),\n signal: props.signal\n });\n\n if (!response.ok) {\n throw await errorFrom(response, \"The AI service could not complete this request.\");\n }\n\n let text = \"\";\n for await (const { event, data } of readServerSentEvents(response)) {\n let payload: any;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n if (event === \"error\") {\n throw new Error(payload?.message ?? \"The AI service reported an error.\");\n }\n if (event === \"delta\" && typeof payload?.text === \"string\") {\n text += payload.text;\n props.onDelta(payload.text);\n }\n }\n return text;\n}\n\n/**\n * Sample prompts for the Autofill menu.\n *\n * Failure is deliberately not thrown: the menu has built-in prompts to fall\n * back on, and an empty suggestion list is a far better outcome than an error\n * toast for something nobody asked for.\n */\nexport async function fetchPromptSuggestions(props: {\n entityName: string;\n input?: string;\n endpoint?: string;\n signal?: AbortSignal;\n}): Promise<SamplePromptsResult> {\n try {\n const response = await fetch(endpointOf(props.endpoint, \"/prompts\"), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n entityName: props.entityName,\n input: props.input\n }),\n signal: props.signal\n });\n if (!response.ok) return { prompts: [] };\n const body = await response.json();\n const prompts: string[] = Array.isArray(body?.prompts) ? body.prompts : [];\n return {\n prompts: prompts\n .filter((p): p is string => typeof p === \"string\")\n .map((prompt) => ({ prompt,\ntype: \"sample\" as const }))\n };\n } catch {\n return { prompts: [] };\n }\n}\n","import { getFieldId } from \"@rebasepro/admin\";\nimport { EnumValues, Properties, Property } from \"@rebasepro/types\";\nimport { isPropertyBuilder } from \"@rebasepro/common\";\nimport { InputProperty } from \"../types/data_enhancement_controller\";\nimport { getValueInPath } from \"@rebasepro/utils\";\n\nexport function getSimplifiedProperties<M extends Record<string, any>>(properties: Properties, values: M, path = \"\"): Record<string, InputProperty> {\n if (!properties) return {};\n return Object.entries(properties)\n .map(([key, property]) => {\n if (isPropertyBuilder(property)) return {};\n const fullKey = path ? `${path}.${key}` : key;\n const valueInPath = getValueInPath(values, fullKey);\n return getSimplifiedProperty(property, fullKey, valueInPath)\n })\n .reduce((a, b) => ({ ...a,\n...b }), {});\n}\n\nfunction getSimpleProperty(property: Property): InputProperty {\n const fieldId = getFieldId(property);\n if (!fieldId) {\n console.error(\"No fieldId found for property\", property);\n throw new Error(\"Field id not found\");\n }\n return {\n name: property.name,\n description: property.description,\n type: property.type,\n fieldConfigId: fieldId,\n enum: \"enum\" in property && property.enum\n ? getSimpleEnumValues(property.enum)\n : undefined,\n disabled: Boolean(property.admin?.disabled || property.admin?.readOnly)\n };\n}\n\nfunction getSimplifiedProperty(property: Property, path: string, value?: unknown): Record<string, InputProperty> {\n if (isPropertyBuilder(property)) return {};\n if (property.type === \"array\") {\n\n if (property.of && !Array.isArray(property.of) && !isPropertyBuilder(property.of)) {\n const arrayParentProperty: InputProperty = {\n name: property.name,\n description: property.description,\n type: property.type,\n fieldConfigId: \"repeat\",\n disabled: Boolean(property.admin?.disabled || property.admin?.readOnly),\n of: getSimpleProperty(property.of as Property)\n };\n\n const result = { [path]: arrayParentProperty };\n // if (Array.isArray(value)) {\n // result = {\n // ...result,\n // ...value\n // .map((v, i) => getSimplifiedProperty(property.of, `${path}.${i}`, v))\n // .reduce((a, b) => ({ ...a, ...b }), {})\n // };\n // }\n //\n // const existingValuesCount = Array.isArray(value) ? value.length : 0;\n //\n // const newValuesCount = property.of && !isPropertyBuilder<any, any>(property.of) && (property.of as Property).type === \"map\" ? 1 : 3;\n // result = {\n // ...result,\n // // ...Array.from(Array(newValuesCount))\n // // .map((v, i) => getSimplifiedProperty(property.of, `${path}.${i + existingValuesCount}`, v))\n // // .reduce((a, b) => ({ ...a, ...b }), {})\n // }\n\n return result;\n } else if (property.oneOf) {\n\n const arrayParentProperty: InputProperty = {\n name: property.name,\n description: property.description,\n type: property.type,\n fieldConfigId: \"block\",\n disabled: Boolean(property.admin?.disabled || property.admin?.readOnly),\n oneOf: {\n typeField: property.oneOf.typeField,\n valueField: property.oneOf.valueField,\n properties: Object.entries(property.oneOf.properties)\n .map(([key, prop]) => ({ [key]: getSimpleProperty(prop) }))\n .reduce((a, b) => ({ ...a,\n...b }), {})\n }\n };\n\n if (!Array.isArray(value)) {\n return { [path]: arrayParentProperty };\n }\n\n return value.map((v, i) => {\n if (v == null) return {};\n const typeKey = property.oneOf!.typeField ?? \"type\";\n const oneOfType = v[typeKey];\n const valueKey = property.oneOf!.valueField ?? \"value\";\n const oneOfValue = v[valueKey];\n const childProperty = property.oneOf!.properties[oneOfType];\n if (childProperty === undefined) {\n console.error(`No property found for type ${oneOfType}`, property.oneOf!.properties);\n return {};\n }\n const simplifiedProperty = getSimplifiedProperty(childProperty, `${path}.${i}.${valueKey}`, oneOfValue);\n return {\n [`${path}.${i}.${typeKey}`]: oneOfType,\n ...simplifiedProperty\n };\n }).reduce((a, b) => ({ ...a,\n...b }), { [path]: arrayParentProperty });\n }\n } else if (property.type === \"map\") {\n if (property.properties) {\n const mapProperties: Record<string, InputProperty> = Object.entries(property.properties)\n .map(([key, childProperty]) => {\n const childValue = value && typeof value === \"object\" ? (value as Record<string, unknown>)[key] : undefined;\n return getSimplifiedProperty(childProperty, key, childValue);\n })\n .map(o => attachPathToKeys(o, path))\n .reduce((a, b) => ({ ...a,\n...b }), {});\n\n if (Object.keys(mapProperties).length === 0) return {};\n const mapParentProperty: InputProperty = {\n name: property.name,\n description: property.description,\n type: property.type,\n fieldConfigId: \"group\",\n disabled: Boolean(property.admin?.disabled || property.admin?.readOnly)\n };\n return {\n [path]: mapParentProperty,\n ...mapProperties\n } as Record<string, InputProperty>;\n }\n } else {\n const fieldId = getFieldId(property);\n if (!fieldId) {\n console.warn(`No fieldId found for property ${path} with type ${property.type}`);\n return {};\n }\n return {\n [path]: getSimpleProperty(property)\n };\n }\n return {};\n}\n\n// attach a path to every key in an object\nfunction attachPathToKeys(obj: Record<string, InputProperty>, path = \"\"): Record<string, InputProperty> {\n return Object.entries(obj)\n .map(([key, value]) => {\n const fullKey = path ? `${path}.${key}` : key;\n return { [fullKey]: value };\n })\n .reduce((a, b) => ({ ...a,\n...b }), {});\n}\n\nfunction getSimpleEnumValues(enumValues: EnumValues): string[] {\n if (Array.isArray(enumValues))\n return enumValues.map(v => String(v.id));\n if (typeof enumValues === \"object\")\n return Object.keys(enumValues);\n throw Error(\"getSimpleEnumValues: Invalid enumValues\");\n}\n","export function flatMapEntityValues<M extends object>(values: M, path = \"\"): object {\n if (!values) return {};\n return Object.entries(values).flatMap(([key, value]) => {\n const currentPath = path ? `${path}.${key}` : key;\n if (typeof value === \"object\") {\n return flatMapEntityValues(value, currentPath);\n } else {\n return { [currentPath]: value };\n }\n }).reduce((acc, curr) => ({ ...acc,\n...curr }), {})\n}\n","import React from \"react\";\nimport { autocompleteStream } from \"../api\";\nimport { EditorAIController } from \"@rebasepro/admin\";\n\n/**\n * Inline continuation for the rich-text editor's slash command.\n *\n * No token is threaded through any more. The previous version demanded a\n * Firebase ID token and threw `\"Firebase token is required\"` when it could not\n * get one — in a Rebase app there is no such thing, and the token it actually\n * sent was a Rebase JWT the receiving service had no way to verify. The hosted\n * service authenticates nobody; see `src/api.ts`.\n */\nexport function useEditorAIController({ endpoint }: { endpoint?: string } = {}): EditorAIController {\n return React.useMemo(() => ({\n autocomplete: (textBefore: string, textAfter: string, onUpdate: (delta: string) => void) =>\n autocompleteStream({\n endpoint,\n textBefore,\n textAfter,\n onDelta: onUpdate\n })\n }), [endpoint]);\n}\n","import React, { PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n AutofillReview,\n DataEnhancementController,\n GenerateParams,\n InputProperty,\n ProposedField\n} from \"../types/data_enhancement_controller\";\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { PluginFormActionProps } from \"@rebasepro/admin-types\";\nimport { autofillStream, fetchAiStatus, fetchPromptSuggestions } from \"../api\";\nimport { getSimplifiedProperties } from \"../utils/properties\";\nimport { flatMapEntityValues } from \"../utils/values\";\nimport { useEditorAIController } from \"../editor/useEditorAIController\";\nimport { getValueInPath } from \"@rebasepro/utils\";\n\nconst DataEnhancementControllerContext = React.createContext<DataEnhancementController>(null! as DataEnhancementController);\n\ntype DataEnhancementControllerProviderProps = {\n\n getConfigForPath?: (props: {\n path: string,\n collection: CollectionConfig\n }) => boolean;\n\n endpoint?: string;\n}\n\nexport const useDataEnhancementController = (): DataEnhancementController => useContext(DataEnhancementControllerContext);\n\nfunction getPropertyFromKey(properties: Record<string, InputProperty>, propertyKey: string): InputProperty | undefined {\n if (propertyKey in properties) {\n return properties[propertyKey];\n }\n const split = propertyKey.split(\".\");\n if (split.length === 1) return undefined;\n return getPropertyFromKey(properties, split.slice(0, -1).join(\".\"));\n}\n\n/**\n * Convert a value off the wire into what the form field expects.\n *\n * Only dates need converting: the service answers ISO-8601 strings because JSON\n * has no date type, and handing a date field a string stores the wrong type\n * without complaining. Everything else — strings, numbers, booleans, arrays of\n * scalars — is already the shape the field wants, which is the point of having\n * the service constrain its answer to a schema derived from these properties.\n */\nfunction coerceToProperty(value: unknown, property: InputProperty | undefined): unknown {\n if (property?.type === \"date\" && typeof value === \"string\") {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? undefined : date;\n }\n return value;\n}\n\nexport function DataEnhancementControllerProvider({\n getConfigForPath,\n children,\n endpoint,\n path,\n collection,\n formContext\n}: PropsWithChildren<DataEnhancementControllerProviderProps & PluginFormActionProps>) {\n\n const [allowedHere, setAllowedHere] = useState(false);\n const [serviceAvailable, setServiceAvailable] = useState(false);\n const [review, setReview] = useState<AutofillReview | null>(null);\n\n const properties = useMemo(\n () => getSimplifiedProperties(collection.properties, formContext?.values ?? {}),\n [collection.properties, formContext?.values]\n );\n\n /**\n * Read inside the streaming callbacks, which outlive the render that\n * started the run.\n *\n * The operator is free to keep typing while the model works — nothing here\n * writes to the form — so the callbacks must not close over a stale\n * property map from whichever render happened to kick the run off.\n */\n const propertiesRef = useRef(properties);\n propertiesRef.current = properties;\n\n /** The host app's own opt-out. */\n useEffect(() => {\n if (!getConfigForPath) {\n setAllowedHere(true);\n return;\n }\n setAllowedHere(Boolean(getConfigForPath({ path,\ncollection })));\n }, [getConfigForPath, path, collection]);\n\n /**\n * The service's own availability.\n *\n * Nothing renders until this comes back true. An unreachable host, an\n * unconfigured provider key or an exhausted daily quota all land here, and\n * all of them mean the same thing to the operator: no Autofill button,\n * rather than a button that fails when clicked.\n */\n useEffect(() => {\n if (!allowedHere) return;\n const abort = new AbortController();\n fetchAiStatus({ endpoint,\nsignal: abort.signal })\n .then((status) => setServiceAvailable(status.available))\n .catch(() => setServiceAvailable(false));\n return () => abort.abort();\n }, [allowedHere, endpoint]);\n\n const enabled = allowedHere && serviceAvailable;\n\n /** Add or update one row in the review, preserving arrival order. */\n const upsertField = useCallback((key: string, update: (existing: ProposedField | undefined) => ProposedField) => {\n setReview((current) => {\n if (!current) return current;\n const index = current.fields.findIndex((f) => f.key === key);\n const next = update(index === -1 ? undefined : current.fields[index]);\n const fields = index === -1\n ? [...current.fields, next]\n : current.fields.map((f, i) => (i === index ? next : f));\n return { ...current,\nfields };\n });\n }, []);\n\n const generate = useCallback(async (params: GenerateParams<Record<string, unknown>>): Promise<void> => {\n\n const currentProperties = propertiesRef.current;\n const flatValues = flatMapEntityValues(params.values ?? {}) as Record<string, unknown>;\n\n setReview({\n status: \"generating\",\n fields: [],\n instructions: params.instructions\n });\n\n const labelFor = (key: string) => currentProperties[key]?.name ?? key;\n\n try {\n await autofillStream({\n endpoint,\n request: {\n entityName: collection.singularName ?? collection.name,\n entityDescription: collection.description,\n // Flattened to dotted paths so the keys line up with the\n // property map: the service is told about `seo.title`, so it\n // has to be told the value of `seo.title` too, not of `seo`.\n values: flatValues,\n properties: currentProperties,\n propertyKey: params.propertyKey,\n propertyInstructions: params.propertyInstructions,\n instructions: params.instructions\n },\n onDelta: (key, text) => {\n upsertField(key, (existing) => existing\n ? { ...existing,\nproposed: String(existing.proposed ?? \"\") + text }\n : {\n key,\n label: labelFor(key),\n currentValue: getValueInPath(params.values, key),\n proposed: text,\n pending: true,\n selected: true\n });\n },\n onValue: (key, value) => {\n const coerced = coerceToProperty(value, getPropertyFromKey(currentProperties, key));\n upsertField(key, (existing) => ({\n key,\n label: existing?.label ?? labelFor(key),\n currentValue: existing?.currentValue ?? getValueInPath(params.values, key),\n proposed: coerced,\n pending: false,\n // A row the operator already deselected mid-stream stays\n // deselected when its final value lands.\n selected: existing?.selected ?? true\n }));\n }\n });\n\n setReview((current) => current && {\n ...current,\n status: \"ready\",\n fields: current.fields.map((f) => ({ ...f,\npending: false }))\n });\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : \"Autofill could not be completed\";\n // Kept in the review rather than fired into a snackbar: a run that\n // produced three good fields and then failed should still let the\n // operator apply the three.\n setReview((current) => current && {\n ...current,\n status: \"failed\",\n error: message,\n fields: current.fields.map((f) => ({ ...f,\npending: false }))\n });\n }\n }, [collection, endpoint, upsertField]);\n\n const toggleField = useCallback((key: string) => {\n setReview((current) => current && {\n ...current,\n fields: current.fields.map((f) => (f.key === key ? { ...f,\nselected: !f.selected } : f))\n });\n }, []);\n\n const toggleAll = useCallback((selected: boolean) => {\n setReview((current) => current && {\n ...current,\n fields: current.fields.map((f) => ({ ...f,\nselected }))\n });\n }, []);\n\n const dismissReview = useCallback(() => setReview(null), []);\n\n const applyReview = useCallback(() => {\n setReview((current) => {\n if (!current) return null;\n for (const field of current.fields) {\n if (!field.selected || field.pending) continue;\n if (field.proposed === undefined || field.proposed === null) continue;\n formContext?.setFieldValue(field.key, field.proposed);\n }\n return null;\n });\n }, [formContext]);\n\n const editorAIController = useEditorAIController({ endpoint });\n\n const getSamplePrompts = useCallback(\n (entityName: string, input?: string) => fetchPromptSuggestions({ endpoint,\nentityName,\ninput }),\n [endpoint]\n );\n\n const dataEnhancementController: DataEnhancementController = useMemo(() => ({\n enabled,\n review,\n generate,\n toggleField,\n toggleAll,\n applyReview,\n dismissReview,\n getSamplePrompts,\n editorAIController\n }), [\n enabled,\n review,\n generate,\n toggleField,\n toggleAll,\n applyReview,\n dismissReview,\n getSamplePrompts,\n editorAIController\n ]);\n\n return (\n <DataEnhancementControllerContext.Provider\n value={dataEnhancementController}>\n {children}\n </DataEnhancementControllerContext.Provider>\n );\n}\n","import React from \"react\";\n\nimport {\n Button,\n Checkbox,\n CircularProgress,\n cls,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Separator,\n Typography\n} from \"@rebasepro/ui\";\n\nimport { ProposedField } from \"../types/data_enhancement_controller\";\nimport { useDataEnhancementController } from \"./DataEnhancementControllerProvider\";\n\n/**\n * The review step.\n *\n * Autofill used to write generated text into the live form as it streamed —\n * fields mutating under the cursor, half-written sentences that looked like\n * bugs, and a pile of heuristics deciding whether each token should append to\n * or replace what the operator had already typed. Getting the old value back\n * meant retyping it.\n *\n * So the generated values land here instead. Streaming still happens, and is\n * still worth having — rows appear and fill in as the model works, so a long\n * run shows progress — but it happens in a surface that owns nothing. The\n * record changes on **Apply**, once, for the rows still ticked.\n */\nexport function AutofillReviewDialog() {\n\n const controller = useDataEnhancementController();\n const review = controller?.review;\n\n if (!review) return null;\n\n const generating = review.status === \"generating\";\n const applicable = review.fields.filter((f) => !f.pending && f.selected);\n const allSelected = review.fields.length > 0 && review.fields.every((f) => f.selected);\n\n return (\n <Dialog\n open={true}\n maxWidth={\"2xl\"}\n onOpenChange={(open) => {\n if (!open) controller.dismissReview();\n }}>\n\n <DialogTitle variant={\"subtitle1\"} gutterBottom={false}>\n Review autofill\n </DialogTitle>\n\n <DialogContent className={\"flex flex-col gap-2\"}>\n\n {review.instructions && (\n <Typography variant={\"body2\"} color={\"secondary\"} className={\"italic\"}>\n “{review.instructions}”\n </Typography>\n )}\n\n {review.fields.length > 1 && (\n <>\n <label className={\"flex items-center gap-3 py-1 cursor-pointer select-none\"}>\n <Checkbox\n checked={allSelected}\n size={\"small\"}\n onCheckedChange={() => controller.toggleAll(!allSelected)}\n />\n <Typography variant={\"label\"} color={\"secondary\"}>\n {allSelected ? \"Deselect all\" : \"Select all\"}\n </Typography>\n </label>\n <Separator orientation={\"horizontal\"} className={\"my-0\"}/>\n </>\n )}\n\n <div className={\"flex flex-col divide-y divide-surface-accent-100 dark:divide-surface-accent-800\"}>\n {review.fields.map((field) => (\n <ProposedFieldRow\n key={field.key}\n field={field}\n onToggle={() => controller.toggleField(field.key)}\n />\n ))}\n </div>\n\n {generating && (\n <div className={\"flex items-center gap-3 py-4 text-text-secondary dark:text-text-secondary-dark\"}>\n <CircularProgress size={\"smallest\"}/>\n <Typography variant={\"body2\"} color={\"secondary\"}>\n {review.fields.length === 0 ? \"Thinking…\" : \"Writing the remaining fields…\"}\n </Typography>\n </div>\n )}\n\n {review.status === \"failed\" && (\n <Typography variant={\"body2\"} className={\"py-2 text-red-600 dark:text-red-400\"}>\n {review.error}\n {review.fields.length > 0 && \" You can still apply what was written before it stopped.\"}\n </Typography>\n )}\n\n {!generating && review.fields.length === 0 && review.status !== \"failed\" && (\n <Typography variant={\"body2\"} color={\"secondary\"} className={\"py-4\"}>\n Nothing to fill in — every field either already has a value the model would not\n improve on, or is not one it can write.\n </Typography>\n )}\n\n </DialogContent>\n\n <DialogActions>\n <Button variant={\"text\"}\n color={\"neutral\"}\n onClick={controller.dismissReview}>\n {/* Named for what it does to the record, not to the dialog:\n nothing has been written, so there is nothing to undo. */}\n Discard\n </Button>\n <Button variant={\"filled\"}\n disabled={applicable.length === 0}\n onClick={controller.applyReview}>\n {applicable.length === 1 ? \"Apply 1 field\" : `Apply ${applicable.length} fields`}\n </Button>\n </DialogActions>\n\n </Dialog>\n );\n}\n\nfunction ProposedFieldRow({ field, onToggle }: { field: ProposedField, onToggle: () => void }) {\n\n const replaces = hasValue(field.currentValue) && !isSameValue(field.currentValue, field.proposed);\n\n return (\n <label className={cls(\n \"flex items-start gap-3 py-3 cursor-pointer\",\n !field.selected && \"opacity-50\"\n )}>\n <div className={\"mt-0.5 shrink-0\"}>\n <Checkbox\n checked={field.selected}\n size={\"small\"}\n onCheckedChange={onToggle}\n />\n </div>\n\n <div className={\"flex flex-col gap-1 min-w-0 grow\"}>\n <div className={\"flex items-center gap-2\"}>\n <Typography variant={\"label\"}>{field.label}</Typography>\n {replaces && (\n <Typography variant={\"caption\"} color={\"secondary\"}>\n replaces the current value\n </Typography>\n )}\n {field.pending && <CircularProgress size={\"smallest\"}/>}\n </div>\n\n {replaces && (\n <Typography\n variant={\"body2\"}\n color={\"secondary\"}\n className={\"line-through whitespace-pre-wrap break-words\"}>\n {renderValue(field.currentValue)}\n </Typography>\n )}\n\n <Typography variant={\"body2\"} className={\"whitespace-pre-wrap break-words\"}>\n {renderValue(field.proposed)}\n </Typography>\n </div>\n </label>\n );\n}\n\nfunction hasValue(value: unknown): boolean {\n if (value === null || value === undefined) return false;\n if (typeof value === \"string\") return value.trim().length > 0;\n if (Array.isArray(value)) return value.length > 0;\n return true;\n}\n\nfunction isSameValue(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => isSameValue(v, b[i]));\n }\n return false;\n}\n\n/** Values are shown, never edited here — so a readable string is all that is needed. */\nfunction renderValue(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (value instanceof Date) return value.toLocaleString();\n if (Array.isArray(value)) return value.map((v) => renderValue(v)).join(\", \");\n if (typeof value === \"boolean\") return value ? \"Yes\" : \"No\";\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n","import React, { useCallback, useEffect, useRef } from \"react\";\n\nimport {\n Button,\n CircularProgress,\n cls,\n focusedDisabled,\n IconButton,\n iconSize,\n Menu,\n MenuItem,\n SendIcon,\n Separator,\n TextareaAutosize,\n XIcon\n} from \"@rebasepro/ui\";\nimport {\n AIIcon\n} from \"@rebasepro/app\";\nimport { EntityStatus, Properties, Property } from \"@rebasepro/types\";\nimport { PluginFormActionProps } from \"@rebasepro/admin-types\";\nimport { isPropertyBuilder, stripCollectionPath } from \"@rebasepro/common\";\nimport { useDataEnhancementController } from \"./DataEnhancementControllerProvider\";\nimport { AutofillReviewDialog } from \"./AutofillReviewDialog\";\nimport { SamplePrompt } from \"../types/data_enhancement_controller\";\n\nexport function FormEnhanceAction({\n path,\n status,\n collection,\n formContext\n}: PluginFormActionProps) {\n\n const storageKey = createLocalStorageKey(path, status);\n\n const dataEnhancementController = useDataEnhancementController();\n\n const [samplePrompts, setSamplePrompts] = React.useState<SamplePrompt[] | undefined>(undefined);\n const [instructions, setInstructions] = React.useState<string>(\"\");\n\n const getSamplePrompts = dataEnhancementController?.getSamplePrompts;\n\n /**\n * Driven by the controller rather than by local state.\n *\n * There is exactly one run at a time, and the review owns it — a second\n * `loading` flag here could disagree with the dialog about whether the\n * model is still writing.\n */\n const loading = dataEnhancementController?.review?.status === \"generating\";\n\n const loadingPrompts = useRef(false);\n const updateSuggestedPrompts = useCallback(async function updateSuggestedPrompts(instructions?: string) {\n if (!getSamplePrompts) return;\n if (loadingPrompts.current) return;\n loadingPrompts.current = true;\n const prompts = status === \"new\"\n ? (await getSamplePrompts(collection.singularName ?? collection.name, instructions)).prompts\n : getPromptsForExistingEntities(collection.properties);\n\n const recentPromptsFromStorage = getRecentPromptsFromStorage(storageKey);\n const recentPrompts = recentPromptsFromStorage.map(prompt => prompt.prompt);\n setSamplePrompts([...recentPromptsFromStorage, ...prompts.filter(p => !recentPrompts.includes(p.prompt))].slice(0, 5));\n loadingPrompts.current = false;\n },\n [collection.name, collection.singularName, getSamplePrompts, status]);\n\n useEffect(() => {\n if (!dataEnhancementController) return;\n if (!samplePrompts) {\n setSamplePrompts(getRecentPromptsFromStorage(storageKey));\n updateSuggestedPrompts().then();\n }\n }, [dataEnhancementController, samplePrompts, storageKey, updateSuggestedPrompts, instructions, status]);\n\n useEffect(() => {\n if (!dataEnhancementController) return;\n updateSuggestedPrompts().then();\n }, [dataEnhancementController, status]);\n\n /**\n * Starts a run and opens the review. Nothing is written to the form here —\n * see {@link AutofillReviewDialog}.\n */\n const generate = (prompt?: string) => {\n if (!dataEnhancementController || !formContext?.values) return;\n if (prompt) {\n addRecentPrompt(storageKey, prompt);\n setSamplePrompts([{\n prompt,\n type: \"recent\"\n }, ...(samplePrompts ?? []).slice(0, 5)]);\n }\n // The controller records a failure in the review itself, so there is\n // nothing to catch here — but the promise is still explicitly handled\n // so a rejection can never surface as an unhandled one.\n dataEnhancementController.generate({\n values: formContext.values,\n instructions: prompt\n }).catch(() => undefined);\n };\n\n if (!dataEnhancementController?.enabled)\n return null;\n\n function submit() {\n generate(instructions);\n }\n\n return (\n <>\n <Menu\n align={\"end\"}\n sideOffset={8}\n className={\"max-w-[100vw]\"}\n // Never full width: this used to stretch to fill the form's\n // `w-80 2xl:w-96` side rail in full screen. That rail is gone, and\n // in the footer a stretched button reads as the primary action.\n trigger={<Button variant={\"filled\"}\n color={\"neutral\"}\n size={\"small\"}\n disabled={loading}>\n {!loading && <AIIcon size={\"small\"}/>}\n {loading && <CircularProgress size={\"small\"}/>}\n Autofill\n </Button>}>\n\n <MenuItem className={\"py-4\"}\n onClick={() => {\n generate();\n }}>\n <AIIcon size={\"small\"}/>\n Autofill based on the current content\n </MenuItem>\n\n <Separator orientation={\"horizontal\"} className={\"mt-2\"}/>\n\n {samplePrompts?.map((samplePrompt, index) => {\n return <MenuItem\n key={index + \"_\" + samplePrompt.prompt}\n onClick={() => {\n setInstructions(samplePrompt.prompt);\n generate(samplePrompt.prompt);\n }}\n >\n <div className={\"pl-9 grow text-text-secondary dark:text-text-secondary-dark\"}>\n {samplePrompt.prompt}\n </div>\n\n {samplePrompt.type === \"recent\" && <IconButton\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n removeRecentPrompt(storageKey, samplePrompt.prompt);\n setSamplePrompts((samplePrompts ?? []).filter(p => p.prompt !== samplePrompt.prompt));\n }}\n size={\"smallest\"}\n >\n <XIcon size={iconSize.smallest}/>\n </IconButton>\n }\n </MenuItem>;\n })}\n\n <Separator orientation={\"horizontal\"}/>\n\n <div\n className={cls(\n \"my-2 w-[500px] max-w-full flex items-start text-surface-700 dark:text-surface-200\"\n )}>\n\n <TextareaAutosize\n 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)}\n value={instructions}\n autoFocus={status === \"new\"}\n disabled={loading}\n onFocus={(event) => {\n event.stopPropagation();\n }}\n placeholder={\"...or provide instructions\"}\n onKeyDown={(e) => {\n e.stopPropagation();\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n submit();\n }\n\n }}\n onChange={(e) => {\n setInstructions(e.target.value);\n }}\n />\n\n <IconButton\n size={\"small\"}\n onClick={() => {\n setInstructions(\"\");\n }}\n color={!instructions ? \"primary\" : undefined}\n disabled={loading || !instructions}>\n <XIcon size={iconSize.small}/>\n </IconButton>\n\n <IconButton\n onClick={() => generate(instructions)}\n size={\"small\"}\n color={!instructions ? \"primary\" : undefined}\n disabled={loading || !instructions}>\n {loading &&\n <CircularProgress size={\"smallest\"}/>}\n {!loading &&\n <SendIcon color={\"primary\"}/>}\n </IconButton>\n\n </div>\n\n </Menu>\n\n <AutofillReviewDialog/>\n </>\n );\n}\n\nfunction getPromptsForExistingEntities(properties: Properties): SamplePrompt[] {\n\n const multilineProperties = Object.values(properties).filter((p: Property) => {\n if (isPropertyBuilder(p)) {\n return false;\n }\n return p.type === \"string\" && (p.admin?.markdown || p.admin?.multiline);\n });\n\n const multilinePrompt: Property | undefined = multilineProperties.length > 0\n ? multilineProperties[Math.floor(Math.random() * multilineProperties.length)] as Property\n : undefined;\n\n const prompts = [\n \"Fill the missing fields\",\n \"Translate the missing content\"\n ];\n if (multilinePrompt) {\n prompts.push(`Add 2 paragraphs to '${multilinePrompt.name}'`);\n }\n return prompts.map(p => ({\n prompt: p,\n type: \"sample\"\n }));\n}\n\nconst createLocalStorageKey = (path: string, status: EntityStatus) => {\n const statusString = status === \"new\" ? \"new\" : \"existing\";\n return `data_enhancement::${statusString}::${stripCollectionPath(path)}`;\n};\n\nconst getRecentPromptsFromStorage = (storageKey: string): SamplePrompt[] => {\n const item = localStorage.getItem(storageKey);\n return item ? JSON.parse(item).map((e: string) => ({\n prompt: e,\n type: \"recent\"\n })) : [];\n};\n\nconst addRecentPrompt = (storageKey: string, prompt: string) => {\n if (!prompt || prompt.trim().length === 0) {\n return;\n }\n const recentPrompts = getRecentPromptsFromStorage(storageKey);\n localStorage.setItem(storageKey, JSON.stringify([prompt, ...recentPrompts\n .map(e => e.prompt)\n .filter(e => e !== prompt)\n .slice(0, 5)]));\n};\n\nconst removeRecentPrompt = (storageKey: string, prompt: string) => {\n localStorage.setItem(storageKey, JSON.stringify(getRecentPromptsFromStorage(storageKey)\n .map(e => e.prompt)\n .filter(e => e !== prompt)));\n};\n","import React from \"react\";\n\nimport { CollectionConfig, User } from \"@rebasepro/types\";\nimport { RebasePlugin } from \"@rebasepro/admin-types\";\nimport { DataEnhancementControllerProvider } from \"./components/DataEnhancementControllerProvider\";\nimport { FormEnhanceAction } from \"./components/FormEnhanceAction\";\n\nexport interface DataEnhancementPluginProps {\n\n /**\n * Use this function to determine if the data enhancement plugin should be enabled for a given path.\n * If this function is not provided, the plugin will be enabled for all paths.\n * If the function returns false, the plugin will be disabled for the given path.\n *\n * @param path\n * @param collection\n */\n getConfigForPath?: (props: {\n path: string,\n collection: CollectionConfig,\n user: User | null\n }) => boolean;\n\n /**\n * Base URL of the AI service.\n *\n * Defaults to the one Rebase hosts, which is free to use and needs no\n * configuration. Point it at your own deployment to keep generation inside\n * your infrastructure — the wire format is documented in `src/api.ts`, and\n * the reference implementation is `saas/backend/functions/ai.ts`.\n *\n * Whatever it points at, the plugin renders nothing until that host's\n * `GET /status` reports itself available.\n */\n endpoint?: string;\n}\n\n/**\n * Use this hook to initialise the data enhancement plugin.\n * This is likely the only hook you will need to use.\n * @param props\n */\nexport function useDataEnhancementPlugin(props?: DataEnhancementPluginProps): RebasePlugin {\n\n const getConfigForPath = props?.getConfigForPath;\n const endpoint = props?.endpoint;\n\n return React.useMemo(() => ({\n key: \"data_enhancement\",\n slots: [\n {\n slot: \"form.actions\",\n Component: FormEnhanceAction,\n order: 40\n }\n ],\n providers: [\n {\n scope: \"form\" as const,\n Component: DataEnhancementControllerProvider as React.ComponentType<any>,\n props: {\n getConfigForPath,\n endpoint\n }\n }\n ]\n }), [getConfigForPath, endpoint]);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","names":[],"sources":["../src/api.ts","../src/utils/properties.ts","../src/utils/values.ts","../src/editor/useEditorAIController.tsx","../src/components/DataEnhancementControllerProvider.tsx","../src/components/AutofillReviewDialog.tsx","../src/components/FormEnhanceAction.tsx","../src/useDataEnhancementPlugin.tsx"],"sourcesContent":["import {\n AutofillRequest,\n AutofillResult,\n AiStatus,\n SamplePromptsResult\n} from \"./types/data_enhancement_controller\";\n\n/**\n * The hosted service Rebase runs for this plugin.\n *\n * The previous value here was `https://api.rebase.pro`, a FireCMS-era host that\n * resolves but serves nothing — every path 404s — so Autofill had never worked\n * in a Rebase install. This one is served by the control plane\n * (`saas/backend/functions/ai.ts`). Point `endpoint` somewhere else to run your\n * own; the wire format below is the whole contract.\n */\nexport const DEFAULT_AI_ENDPOINT = \"https://app.rebase.pro/api/functions/ai\";\n\n/**\n * ## No credentials cross this boundary\n *\n * The old client sent the tenant's Rebase JWT as `Authorization: Basic <jwt>`\n * plus a hardcoded `fcms-…` key compiled into the published package. Both were\n * wrong in the same way: a self-hosted backend signs its tokens with its own\n * secret, so no external service can verify one — sending it only handed a live\n * credential to a third party that had no use for it.\n *\n * These requests are anonymous. The service bounds cost by rate limit and daily\n * ceiling rather than by identity, and reports through {@link fetchAiStatus}\n * when it can no longer serve — which is what keeps the UI from offering an\n * action that is going to fail.\n */\nfunction endpointOf(endpoint: string | undefined, path: string): string {\n return (endpoint ?? DEFAULT_AI_ENDPOINT).replace(/\\/+$/, \"\") + path;\n}\n\n/** One `event:`/`data:` pair off the wire. */\ntype ServerSentEvent = { event: string; data: string };\n\n/** Not global: `exec` must not carry `lastIndex` between buffer reads. */\nconst SSE_SEPARATOR = /\\r?\\n\\r?\\n/;\n\n/**\n * Parse an SSE body incrementally.\n *\n * The framing this replaces split each chunk on the literal `\"&$# \"` and\n * `JSON.parse`d the pieces, which corrupted itself the moment a delimiter\n * straddled two reads — and network reads land wherever they land. Buffering\n * until a blank line is the fix, and it is also just what SSE specifies.\n */\nasync function* readServerSentEvents(response: Response): AsyncGenerator<ServerSentEvent> {\n const reader = response.body?.getReader();\n if (!reader) throw new Error(\"The AI service returned no response body\");\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n // A record ends at a blank line. `\\r\\n` is tolerated because proxies\n // rewrite line endings. The separator is located with `exec` rather\n // than `search` so its actual length is known — a `\\r\\n\\r\\n` boundary\n // is four characters, not two, and slicing by the wrong count leaves a\n // stray newline that swallows the next record's `event:` field.\n let match = SSE_SEPARATOR.exec(buffer);\n while (match) {\n const raw = buffer.slice(0, match.index);\n buffer = buffer.slice(match.index + match[0].length);\n const parsed = parseEventBlock(raw);\n if (parsed) yield parsed;\n match = SSE_SEPARATOR.exec(buffer);\n }\n }\n}\n\nfunction parseEventBlock(block: string): ServerSentEvent | undefined {\n let event = \"message\";\n const dataLines: string[] = [];\n for (const line of block.split(/\\r?\\n/)) {\n if (line.startsWith(\":\")) continue; // comment / keep-alive\n const separator = line.indexOf(\":\");\n const field = separator === -1 ? line : line.slice(0, separator);\n const rawValue = separator === -1 ? \"\" : line.slice(separator + 1);\n const value = rawValue.startsWith(\" \") ? rawValue.slice(1) : rawValue;\n if (field === \"event\") event = value;\n else if (field === \"data\") dataLines.push(value);\n }\n if (dataLines.length === 0) return undefined;\n return { event,\ndata: dataLines.join(\"\\n\") };\n}\n\n/** Pull a message out of the control plane's `{ error: { message } }` envelope. */\nasync function errorFrom(response: Response, fallback: string): Promise<Error> {\n try {\n const body = await response.json();\n const message = body?.error?.message;\n if (typeof message === \"string\" && message) return new Error(message);\n } catch {\n /* not JSON — fall through */\n }\n return new Error(fallback);\n}\n\n/**\n * Ask the service whether it can serve a request at all.\n *\n * The plugin gates every affordance on this. A missing provider key, an\n * exhausted daily quota or an unreachable host all resolve to `available:\n * false`, and the Autofill button is simply not rendered — rather than\n * rendered, clicked, and failed.\n */\nexport async function fetchAiStatus(props: { endpoint?: string; signal?: AbortSignal }): Promise<AiStatus> {\n const response = await fetch(endpointOf(props.endpoint, \"/status\"), {\n method: \"GET\",\n signal: props.signal\n });\n if (!response.ok) return { available: false };\n const body = await response.json();\n return {\n available: Boolean(body?.available),\n model: typeof body?.model === \"string\" ? body.model : undefined,\n features: Array.isArray(body?.features) ? body.features : undefined\n };\n}\n\n/**\n * Fill a record, streaming each field as the service writes it.\n *\n * `onDelta` fires with more text for a field still being written; `onValue`\n * fires once a field is complete and carries its final, correctly typed value.\n * A caller that implements only `onValue` still ends up with the right record —\n * the deltas exist so a long text field fills in visibly instead of appearing\n * all at once.\n */\nexport async function autofillStream(props: {\n request: AutofillRequest;\n endpoint?: string;\n signal?: AbortSignal;\n onDelta: (key: string, text: string) => void;\n onValue: (key: string, value: unknown) => void;\n}): Promise<AutofillResult> {\n const response = await fetch(endpointOf(props.endpoint, \"/autofill\"), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(props.request),\n signal: props.signal\n });\n\n if (!response.ok) {\n throw await errorFrom(response, \"The AI service could not complete this request.\");\n }\n\n let result: AutofillResult = { suggestions: {} };\n\n for await (const { event, data } of readServerSentEvents(response)) {\n let payload: any;\n try {\n payload = JSON.parse(data);\n } catch {\n // One malformed record must not abort a stream that is otherwise\n // delivering good fields.\n continue;\n }\n\n if (event === \"suggestion_delta\") {\n props.onDelta(payload.key, payload.text);\n } else if (event === \"suggestion\") {\n props.onValue(payload.key, payload.value);\n } else if (event === \"done\") {\n result = {\n suggestions: payload.suggestions ?? {},\n usage: payload.usage\n };\n } else if (event === \"error\") {\n throw new Error(payload.message ?? \"The AI service reported an error.\");\n }\n }\n\n return result;\n}\n\n/** Inline continuation for the rich-text editor. Streams plain text. */\nexport async function autocompleteStream(props: {\n textBefore?: string;\n textAfter?: string;\n endpoint?: string;\n signal?: AbortSignal;\n onDelta: (text: string) => void;\n}): Promise<string> {\n const response = await fetch(endpointOf(props.endpoint, \"/autocomplete\"), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n textBefore: props.textBefore ?? \"\",\n textAfter: props.textAfter ?? \"\"\n }),\n signal: props.signal\n });\n\n if (!response.ok) {\n throw await errorFrom(response, \"The AI service could not complete this request.\");\n }\n\n let text = \"\";\n for await (const { event, data } of readServerSentEvents(response)) {\n let payload: any;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n if (event === \"error\") {\n throw new Error(payload?.message ?? \"The AI service reported an error.\");\n }\n if (event === \"delta\" && typeof payload?.text === \"string\") {\n text += payload.text;\n props.onDelta(payload.text);\n }\n }\n return text;\n}\n\n/**\n * Sample prompts for the Autofill menu.\n *\n * Failure is deliberately not thrown: the menu has built-in prompts to fall\n * back on, and an empty suggestion list is a far better outcome than an error\n * toast for something nobody asked for.\n */\nexport async function fetchPromptSuggestions(props: {\n entityName: string;\n /** Ties suggestions to the domain — see the note on the service's side. */\n entityDescription?: string;\n input?: string;\n endpoint?: string;\n signal?: AbortSignal;\n}): Promise<SamplePromptsResult> {\n try {\n const response = await fetch(endpointOf(props.endpoint, \"/prompts\"), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n entityName: props.entityName,\n entityDescription: props.entityDescription,\n input: props.input\n }),\n signal: props.signal\n });\n if (!response.ok) return { prompts: [] };\n const body = await response.json();\n const prompts: string[] = Array.isArray(body?.prompts) ? body.prompts : [];\n return {\n prompts: prompts\n .filter((p): p is string => typeof p === \"string\")\n .map((prompt) => ({ prompt,\ntype: \"sample\" as const }))\n };\n } catch {\n return { prompts: [] };\n }\n}\n","import { getFieldId } from \"@rebasepro/admin\";\nimport { EnumValues, Properties, Property } from \"@rebasepro/types\";\nimport { isPropertyBuilder } from \"@rebasepro/common\";\nimport { InputProperty } from \"../types/data_enhancement_controller\";\nimport { getValueInPath } from \"@rebasepro/utils\";\n\nexport function getSimplifiedProperties<M extends Record<string, any>>(properties: Properties, values: M, path = \"\"): Record<string, InputProperty> {\n if (!properties) return {};\n return Object.entries(properties)\n .map(([key, property]) => {\n if (isPropertyBuilder(property)) return {};\n const fullKey = path ? `${path}.${key}` : key;\n const valueInPath = getValueInPath(values, fullKey);\n return getSimplifiedProperty(property, fullKey, valueInPath)\n })\n .reduce((a, b) => ({ ...a,\n...b }), {});\n}\n\nfunction getSimpleProperty(property: Property): InputProperty {\n const fieldId = getFieldId(property);\n if (!fieldId) {\n console.error(\"No fieldId found for property\", property);\n throw new Error(\"Field id not found\");\n }\n return {\n name: property.name,\n description: property.description,\n type: property.type,\n fieldConfigId: fieldId,\n enum: \"enum\" in property && property.enum\n ? getSimpleEnumValues(property.enum)\n : undefined,\n disabled: Boolean(property.admin?.disabled || property.admin?.readOnly)\n };\n}\n\nfunction getSimplifiedProperty(property: Property, path: string, value?: unknown): Record<string, InputProperty> {\n if (isPropertyBuilder(property)) return {};\n if (property.type === \"array\") {\n\n if (property.of && !Array.isArray(property.of) && !isPropertyBuilder(property.of)) {\n const arrayParentProperty: InputProperty = {\n name: property.name,\n description: property.description,\n type: property.type,\n fieldConfigId: \"repeat\",\n disabled: Boolean(property.admin?.disabled || property.admin?.readOnly),\n of: getSimpleProperty(property.of as Property)\n };\n\n const result = { [path]: arrayParentProperty };\n // if (Array.isArray(value)) {\n // result = {\n // ...result,\n // ...value\n // .map((v, i) => getSimplifiedProperty(property.of, `${path}.${i}`, v))\n // .reduce((a, b) => ({ ...a, ...b }), {})\n // };\n // }\n //\n // const existingValuesCount = Array.isArray(value) ? value.length : 0;\n //\n // const newValuesCount = property.of && !isPropertyBuilder<any, any>(property.of) && (property.of as Property).type === \"map\" ? 1 : 3;\n // result = {\n // ...result,\n // // ...Array.from(Array(newValuesCount))\n // // .map((v, i) => getSimplifiedProperty(property.of, `${path}.${i + existingValuesCount}`, v))\n // // .reduce((a, b) => ({ ...a, ...b }), {})\n // }\n\n return result;\n } else if (property.oneOf) {\n\n const arrayParentProperty: InputProperty = {\n name: property.name,\n description: property.description,\n type: property.type,\n fieldConfigId: \"block\",\n disabled: Boolean(property.admin?.disabled || property.admin?.readOnly),\n oneOf: {\n typeField: property.oneOf.typeField,\n valueField: property.oneOf.valueField,\n properties: Object.entries(property.oneOf.properties)\n .map(([key, prop]) => ({ [key]: getSimpleProperty(prop) }))\n .reduce((a, b) => ({ ...a,\n...b }), {})\n }\n };\n\n if (!Array.isArray(value)) {\n return { [path]: arrayParentProperty };\n }\n\n return value.map((v, i) => {\n if (v == null) return {};\n const typeKey = property.oneOf!.typeField ?? \"type\";\n const oneOfType = v[typeKey];\n const valueKey = property.oneOf!.valueField ?? \"value\";\n const oneOfValue = v[valueKey];\n const childProperty = property.oneOf!.properties[oneOfType];\n if (childProperty === undefined) {\n console.error(`No property found for type ${oneOfType}`, property.oneOf!.properties);\n return {};\n }\n const simplifiedProperty = getSimplifiedProperty(childProperty, `${path}.${i}.${valueKey}`, oneOfValue);\n return {\n [`${path}.${i}.${typeKey}`]: oneOfType,\n ...simplifiedProperty\n };\n }).reduce((a, b) => ({ ...a,\n...b }), { [path]: arrayParentProperty });\n }\n } else if (property.type === \"map\") {\n if (property.properties) {\n const mapProperties: Record<string, InputProperty> = Object.entries(property.properties)\n .map(([key, childProperty]) => {\n const childValue = value && typeof value === \"object\" ? (value as Record<string, unknown>)[key] : undefined;\n return getSimplifiedProperty(childProperty, key, childValue);\n })\n .map(o => attachPathToKeys(o, path))\n .reduce((a, b) => ({ ...a,\n...b }), {});\n\n if (Object.keys(mapProperties).length === 0) return {};\n const mapParentProperty: InputProperty = {\n name: property.name,\n description: property.description,\n type: property.type,\n fieldConfigId: \"group\",\n disabled: Boolean(property.admin?.disabled || property.admin?.readOnly)\n };\n return {\n [path]: mapParentProperty,\n ...mapProperties\n } as Record<string, InputProperty>;\n }\n } else {\n const fieldId = getFieldId(property);\n if (!fieldId) {\n console.warn(`No fieldId found for property ${path} with type ${property.type}`);\n return {};\n }\n return {\n [path]: getSimpleProperty(property)\n };\n }\n return {};\n}\n\n// attach a path to every key in an object\nfunction attachPathToKeys(obj: Record<string, InputProperty>, path = \"\"): Record<string, InputProperty> {\n return Object.entries(obj)\n .map(([key, value]) => {\n const fullKey = path ? `${path}.${key}` : key;\n return { [fullKey]: value };\n })\n .reduce((a, b) => ({ ...a,\n...b }), {});\n}\n\nfunction getSimpleEnumValues(enumValues: EnumValues): string[] {\n if (Array.isArray(enumValues))\n return enumValues.map(v => String(v.id));\n if (typeof enumValues === \"object\")\n return Object.keys(enumValues);\n throw Error(\"getSimpleEnumValues: Invalid enumValues\");\n}\n","export function flatMapEntityValues<M extends object>(values: M, path = \"\"): object {\n if (!values) return {};\n return Object.entries(values).flatMap(([key, value]) => {\n const currentPath = path ? `${path}.${key}` : key;\n if (typeof value === \"object\") {\n return flatMapEntityValues(value, currentPath);\n } else {\n return { [currentPath]: value };\n }\n }).reduce((acc, curr) => ({ ...acc,\n...curr }), {})\n}\n","import React from \"react\";\nimport { autocompleteStream } from \"../api\";\nimport { EditorAIController } from \"@rebasepro/admin\";\n\n/**\n * Inline continuation for the rich-text editor's slash command.\n *\n * No token is threaded through any more. The previous version demanded a\n * Firebase ID token and threw `\"Firebase token is required\"` when it could not\n * get one — in a Rebase app there is no such thing, and the token it actually\n * sent was a Rebase JWT the receiving service had no way to verify. The hosted\n * service authenticates nobody; see `src/api.ts`.\n */\nexport function useEditorAIController({ endpoint }: { endpoint?: string } = {}): EditorAIController {\n return React.useMemo(() => ({\n autocomplete: (textBefore: string, textAfter: string, onUpdate: (delta: string) => void) =>\n autocompleteStream({\n endpoint,\n textBefore,\n textAfter,\n onDelta: onUpdate\n })\n }), [endpoint]);\n}\n","import React, { PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n AutofillReview,\n DataEnhancementController,\n GenerateParams,\n InputProperty,\n ProposedField\n} from \"../types/data_enhancement_controller\";\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { PluginFormActionProps } from \"@rebasepro/admin-types\";\nimport { autofillStream, fetchAiStatus, fetchPromptSuggestions } from \"../api\";\nimport { getSimplifiedProperties } from \"../utils/properties\";\nimport { flatMapEntityValues } from \"../utils/values\";\nimport { useEditorAIController } from \"../editor/useEditorAIController\";\nimport { getValueInPath } from \"@rebasepro/utils\";\n\nconst DataEnhancementControllerContext = React.createContext<DataEnhancementController>(null! as DataEnhancementController);\n\ntype DataEnhancementControllerProviderProps = {\n\n getConfigForPath?: (props: {\n path: string,\n collection: CollectionConfig\n }) => boolean;\n\n endpoint?: string;\n}\n\nexport const useDataEnhancementController = (): DataEnhancementController => useContext(DataEnhancementControllerContext);\n\nfunction getPropertyFromKey(properties: Record<string, InputProperty>, propertyKey: string): InputProperty | undefined {\n if (propertyKey in properties) {\n return properties[propertyKey];\n }\n const split = propertyKey.split(\".\");\n if (split.length === 1) return undefined;\n return getPropertyFromKey(properties, split.slice(0, -1).join(\".\"));\n}\n\n/**\n * Convert a value off the wire into what the form field expects.\n *\n * Only dates need converting: the service answers ISO-8601 strings because JSON\n * has no date type, and handing a date field a string stores the wrong type\n * without complaining. Everything else — strings, numbers, booleans, arrays of\n * scalars — is already the shape the field wants, which is the point of having\n * the service constrain its answer to a schema derived from these properties.\n */\nfunction coerceToProperty(value: unknown, property: InputProperty | undefined): unknown {\n if (property?.type === \"date\" && typeof value === \"string\") {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? undefined : date;\n }\n return value;\n}\n\nexport function DataEnhancementControllerProvider({\n getConfigForPath,\n children,\n endpoint,\n path,\n collection,\n formContext\n}: PropsWithChildren<DataEnhancementControllerProviderProps & PluginFormActionProps>) {\n\n const [allowedHere, setAllowedHere] = useState(false);\n const [serviceAvailable, setServiceAvailable] = useState(false);\n const [review, setReview] = useState<AutofillReview | null>(null);\n\n const properties = useMemo(\n () => getSimplifiedProperties(collection.properties, formContext?.values ?? {}),\n [collection.properties, formContext?.values]\n );\n\n /**\n * Read inside the streaming callbacks, which outlive the render that\n * started the run.\n *\n * The operator is free to keep typing while the model works — nothing here\n * writes to the form — so the callbacks must not close over a stale\n * property map from whichever render happened to kick the run off.\n */\n const propertiesRef = useRef(properties);\n propertiesRef.current = properties;\n\n /** The host app's own opt-out. */\n useEffect(() => {\n if (!getConfigForPath) {\n setAllowedHere(true);\n return;\n }\n setAllowedHere(Boolean(getConfigForPath({ path,\ncollection })));\n }, [getConfigForPath, path, collection]);\n\n /**\n * The service's own availability.\n *\n * Nothing renders until this comes back true. An unreachable host, an\n * unconfigured provider key or an exhausted daily quota all land here, and\n * all of them mean the same thing to the operator: no Autofill button,\n * rather than a button that fails when clicked.\n */\n useEffect(() => {\n if (!allowedHere) return;\n const abort = new AbortController();\n fetchAiStatus({ endpoint,\nsignal: abort.signal })\n .then((status) => setServiceAvailable(status.available))\n .catch(() => setServiceAvailable(false));\n return () => abort.abort();\n }, [allowedHere, endpoint]);\n\n const enabled = allowedHere && serviceAvailable;\n\n /** Add or update one row in the review, preserving arrival order. */\n const upsertField = useCallback((key: string, update: (existing: ProposedField | undefined) => ProposedField) => {\n setReview((current) => {\n if (!current) return current;\n const index = current.fields.findIndex((f) => f.key === key);\n const next = update(index === -1 ? undefined : current.fields[index]);\n const fields = index === -1\n ? [...current.fields, next]\n : current.fields.map((f, i) => (i === index ? next : f));\n return { ...current,\nfields };\n });\n }, []);\n\n const generate = useCallback(async (params: GenerateParams<Record<string, unknown>>): Promise<void> => {\n\n const currentProperties = propertiesRef.current;\n const flatValues = flatMapEntityValues(params.values ?? {}) as Record<string, unknown>;\n\n setReview({\n status: \"generating\",\n fields: [],\n instructions: params.instructions\n });\n\n const labelFor = (key: string) => currentProperties[key]?.name ?? key;\n\n try {\n await autofillStream({\n endpoint,\n request: {\n entityName: collection.singularName ?? collection.name,\n entityDescription: collection.description,\n // Flattened to dotted paths so the keys line up with the\n // property map: the service is told about `seo.title`, so it\n // has to be told the value of `seo.title` too, not of `seo`.\n values: flatValues,\n properties: currentProperties,\n propertyKey: params.propertyKey,\n propertyInstructions: params.propertyInstructions,\n instructions: params.instructions\n },\n onDelta: (key, text) => {\n upsertField(key, (existing) => existing\n ? { ...existing,\nproposed: String(existing.proposed ?? \"\") + text }\n : {\n key,\n label: labelFor(key),\n currentValue: getValueInPath(params.values, key),\n proposed: text,\n pending: true,\n selected: true\n });\n },\n onValue: (key, value) => {\n const coerced = coerceToProperty(value, getPropertyFromKey(currentProperties, key));\n upsertField(key, (existing) => ({\n key,\n label: existing?.label ?? labelFor(key),\n currentValue: existing?.currentValue ?? getValueInPath(params.values, key),\n proposed: coerced,\n pending: false,\n // A row the operator already deselected mid-stream stays\n // deselected when its final value lands.\n selected: existing?.selected ?? true\n }));\n }\n });\n\n setReview((current) => current && {\n ...current,\n status: \"ready\",\n // Fields still pending when the run ended never received a final\n // value — the model's JSON was cut off mid-string, so all we\n // hold is a half-written sentence. Marking them complete would\n // make that sentence applicable, which is the exact outcome the\n // review exists to prevent. They are dropped instead: the review\n // only ever offers what the service actually finished.\n fields: current.fields.filter((f) => !f.pending)\n });\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : \"Autofill could not be completed\";\n // Kept in the review rather than fired into a snackbar: a run that\n // produced three good fields and then failed should still let the\n // operator apply the three.\n setReview((current) => current && {\n ...current,\n status: \"failed\",\n error: message,\n // Same rule as the success path: a field interrupted mid-write\n // is not something the operator can be offered.\n fields: current.fields.filter((f) => !f.pending)\n });\n }\n }, [collection, endpoint, upsertField]);\n\n const toggleField = useCallback((key: string) => {\n setReview((current) => current && {\n ...current,\n fields: current.fields.map((f) => (f.key === key ? { ...f,\nselected: !f.selected } : f))\n });\n }, []);\n\n const toggleAll = useCallback((selected: boolean) => {\n setReview((current) => current && {\n ...current,\n fields: current.fields.map((f) => ({ ...f,\nselected }))\n });\n }, []);\n\n const dismissReview = useCallback(() => setReview(null), []);\n\n const applyReview = useCallback(() => {\n setReview((current) => {\n if (!current) return null;\n for (const field of current.fields) {\n if (!field.selected || field.pending) continue;\n if (field.proposed === undefined || field.proposed === null) continue;\n formContext?.setFieldValue(field.key, field.proposed);\n }\n return null;\n });\n }, [formContext]);\n\n const editorAIController = useEditorAIController({ endpoint });\n\n const getSamplePrompts = useCallback(\n (entityName: string, input?: string) => fetchPromptSuggestions({\n endpoint,\n entityName,\n entityDescription: collection.description,\n input\n }),\n [endpoint, collection.description]\n );\n\n const dataEnhancementController: DataEnhancementController = useMemo(() => ({\n enabled,\n review,\n generate,\n toggleField,\n toggleAll,\n applyReview,\n dismissReview,\n getSamplePrompts,\n editorAIController\n }), [\n enabled,\n review,\n generate,\n toggleField,\n toggleAll,\n applyReview,\n dismissReview,\n getSamplePrompts,\n editorAIController\n ]);\n\n return (\n <DataEnhancementControllerContext.Provider\n value={dataEnhancementController}>\n {children}\n </DataEnhancementControllerContext.Provider>\n );\n}\n","import React from \"react\";\n\nimport {\n Button,\n Checkbox,\n CircularProgress,\n cls,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Separator,\n Typography\n} from \"@rebasepro/ui\";\n\nimport { ProposedField } from \"../types/data_enhancement_controller\";\nimport { useDataEnhancementController } from \"./DataEnhancementControllerProvider\";\n\n/**\n * The review step.\n *\n * Autofill used to write generated text into the live form as it streamed —\n * fields mutating under the cursor, half-written sentences that looked like\n * bugs, and a pile of heuristics deciding whether each token should append to\n * or replace what the operator had already typed. Getting the old value back\n * meant retyping it.\n *\n * So the generated values land here instead. Streaming still happens, and is\n * still worth having — rows appear and fill in as the model works, so a long\n * run shows progress — but it happens in a surface that owns nothing. The\n * record changes on **Apply**, once, for the rows still ticked.\n */\nexport function AutofillReviewDialog() {\n\n const controller = useDataEnhancementController();\n const review = controller?.review;\n\n if (!review) return null;\n\n const generating = review.status === \"generating\";\n const applicable = review.fields.filter((f) => !f.pending && f.selected);\n const allSelected = review.fields.length > 0 && review.fields.every((f) => f.selected);\n\n return (\n <Dialog\n open={true}\n maxWidth={\"2xl\"}\n onOpenChange={(open) => {\n if (!open) controller.dismissReview();\n }}>\n\n <DialogTitle variant={\"subtitle1\"} gutterBottom={false}>\n Review autofill\n </DialogTitle>\n\n <DialogContent className={\"flex flex-col gap-2\"}>\n\n {review.instructions && (\n <Typography variant={\"body2\"} color={\"secondary\"} className={\"italic\"}>\n “{review.instructions}”\n </Typography>\n )}\n\n {review.fields.length > 1 && (\n <>\n <label className={\"flex items-center gap-3 py-1 cursor-pointer select-none\"}>\n <Checkbox\n checked={allSelected}\n size={\"small\"}\n onCheckedChange={() => controller.toggleAll(!allSelected)}\n />\n {/* `component=\"span\"`: the Typography `label`\n variant renders a <label> element, and this sits\n inside the row's own <label>. Nested labels are\n invalid HTML and stop the text toggling the\n checkbox — clicking \"Select all\" did nothing. */}\n <Typography variant={\"label\"} component={\"span\"} color={\"secondary\"}>\n {allSelected ? \"Deselect all\" : \"Select all\"}\n </Typography>\n </label>\n <Separator orientation={\"horizontal\"} className={\"my-0\"}/>\n </>\n )}\n\n <div className={\"flex flex-col divide-y divide-surface-accent-100 dark:divide-surface-accent-800\"}>\n {review.fields.map((field) => (\n <ProposedFieldRow\n key={field.key}\n field={field}\n onToggle={() => controller.toggleField(field.key)}\n />\n ))}\n </div>\n\n {generating && (\n <div className={\"flex items-center gap-3 py-4 text-text-secondary dark:text-text-secondary-dark\"}>\n <CircularProgress size={\"smallest\"}/>\n <Typography variant={\"body2\"} color={\"secondary\"}>\n {review.fields.length === 0 ? \"Thinking…\" : \"Writing the remaining fields…\"}\n </Typography>\n </div>\n )}\n\n {review.status === \"failed\" && (\n <Typography variant={\"body2\"} className={\"py-2 text-red-600 dark:text-red-400\"}>\n {review.error}\n {review.fields.length > 0 && \" You can still apply what was written before it stopped.\"}\n </Typography>\n )}\n\n {!generating && review.fields.length === 0 && review.status !== \"failed\" && (\n <Typography variant={\"body2\"} color={\"secondary\"} className={\"py-4\"}>\n Nothing to fill in — every field either already has a value the model would not\n improve on, or is not one it can write.\n </Typography>\n )}\n\n </DialogContent>\n\n <DialogActions>\n <Button variant={\"text\"}\n color={\"neutral\"}\n onClick={controller.dismissReview}>\n {/* Named for what it does to the record, not to the dialog:\n nothing has been written, so there is nothing to undo. */}\n Discard\n </Button>\n <Button variant={\"filled\"}\n disabled={applicable.length === 0}\n onClick={controller.applyReview}>\n {applicable.length === 1 ? \"Apply 1 field\" : `Apply ${applicable.length} fields`}\n </Button>\n </DialogActions>\n\n </Dialog>\n );\n}\n\nfunction ProposedFieldRow({ field, onToggle }: { field: ProposedField, onToggle: () => void }) {\n\n const replaces = hasValue(field.currentValue) && !isSameValue(field.currentValue, field.proposed);\n\n return (\n <label className={cls(\n \"flex items-start gap-3 py-3 cursor-pointer\",\n !field.selected && \"opacity-50\"\n )}>\n <div className={\"mt-0.5 shrink-0\"}>\n <Checkbox\n checked={field.selected}\n size={\"small\"}\n onCheckedChange={onToggle}\n />\n </div>\n\n <div className={\"flex flex-col gap-1 min-w-0 grow\"}>\n <div className={\"flex items-center gap-2\"}>\n {/* See the note above: never a bare `label` variant inside a <label>. */}\n <Typography variant={\"label\"} component={\"span\"}>{field.label}</Typography>\n {replaces && (\n <Typography variant={\"caption\"} color={\"secondary\"}>\n replaces the current value\n </Typography>\n )}\n {field.pending && <CircularProgress size={\"smallest\"}/>}\n </div>\n\n {replaces && (\n <Typography\n variant={\"body2\"}\n color={\"secondary\"}\n className={\"line-through whitespace-pre-wrap break-words\"}>\n {renderValue(field.currentValue)}\n </Typography>\n )}\n\n <Typography variant={\"body2\"} className={\"whitespace-pre-wrap break-words\"}>\n {renderValue(field.proposed)}\n </Typography>\n </div>\n </label>\n );\n}\n\nfunction hasValue(value: unknown): boolean {\n if (value === null || value === undefined) return false;\n if (typeof value === \"string\") return value.trim().length > 0;\n if (Array.isArray(value)) return value.length > 0;\n return true;\n}\n\nfunction isSameValue(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => isSameValue(v, b[i]));\n }\n return false;\n}\n\n/** Values are shown, never edited here — so a readable string is all that is needed. */\nfunction renderValue(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (value instanceof Date) return value.toLocaleString();\n if (Array.isArray(value)) return value.map((v) => renderValue(v)).join(\", \");\n if (typeof value === \"boolean\") return value ? \"Yes\" : \"No\";\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n","import React, { useCallback, useEffect, useRef } from \"react\";\n\nimport {\n Button,\n CircularProgress,\n cls,\n fieldBackgroundMixin,\n focusedDisabled,\n IconButton,\n iconSize,\n Menu,\n MenuItem,\n SendIcon,\n Separator,\n TextareaAutosize,\n XIcon\n} from \"@rebasepro/ui\";\nimport {\n AIIcon\n} from \"@rebasepro/app\";\nimport { EntityStatus, Properties, Property } from \"@rebasepro/types\";\nimport { PluginFormActionProps } from \"@rebasepro/admin-types\";\nimport { isPropertyBuilder, stripCollectionPath } from \"@rebasepro/common\";\nimport { useDataEnhancementController } from \"./DataEnhancementControllerProvider\";\nimport { AutofillReviewDialog } from \"./AutofillReviewDialog\";\nimport { SamplePrompt } from \"../types/data_enhancement_controller\";\n\nexport function FormEnhanceAction({\n path,\n status,\n collection,\n formContext\n}: PluginFormActionProps) {\n\n const storageKey = createLocalStorageKey(path, status);\n\n const dataEnhancementController = useDataEnhancementController();\n\n const [samplePrompts, setSamplePrompts] = React.useState<SamplePrompt[] | undefined>(undefined);\n const [instructions, setInstructions] = React.useState<string>(\"\");\n\n const getSamplePrompts = dataEnhancementController?.getSamplePrompts;\n\n /**\n * Driven by the controller rather than by local state.\n *\n * There is exactly one run at a time, and the review owns it — a second\n * `loading` flag here could disagree with the dialog about whether the\n * model is still writing.\n */\n const loading = dataEnhancementController?.review?.status === \"generating\";\n\n const loadingPrompts = useRef(false);\n const updateSuggestedPrompts = useCallback(async function updateSuggestedPrompts(instructions?: string) {\n if (!getSamplePrompts) return;\n if (loadingPrompts.current) return;\n loadingPrompts.current = true;\n const prompts = status === \"new\"\n ? (await getSamplePrompts(collection.singularName ?? collection.name, instructions)).prompts\n : getPromptsForExistingEntities(collection.properties);\n\n const recentPromptsFromStorage = getRecentPromptsFromStorage(storageKey);\n const recentPrompts = recentPromptsFromStorage.map(prompt => prompt.prompt);\n setSamplePrompts([...recentPromptsFromStorage, ...prompts.filter(p => !recentPrompts.includes(p.prompt))].slice(0, 5));\n loadingPrompts.current = false;\n },\n [collection.name, collection.singularName, getSamplePrompts, status]);\n\n useEffect(() => {\n if (!dataEnhancementController) return;\n if (!samplePrompts) {\n setSamplePrompts(getRecentPromptsFromStorage(storageKey));\n updateSuggestedPrompts().then();\n }\n }, [dataEnhancementController, samplePrompts, storageKey, updateSuggestedPrompts, instructions, status]);\n\n useEffect(() => {\n if (!dataEnhancementController) return;\n updateSuggestedPrompts().then();\n }, [dataEnhancementController, status]);\n\n /**\n * Starts a run and opens the review. Nothing is written to the form here —\n * see {@link AutofillReviewDialog}.\n */\n const generate = (prompt?: string) => {\n if (!dataEnhancementController || !formContext?.values) return;\n if (prompt) {\n addRecentPrompt(storageKey, prompt);\n setSamplePrompts([{\n prompt,\n type: \"recent\"\n }, ...(samplePrompts ?? []).slice(0, 5)]);\n }\n // The controller records a failure in the review itself, so there is\n // nothing to catch here — but the promise is still explicitly handled\n // so a rejection can never surface as an unhandled one.\n dataEnhancementController.generate({\n values: formContext.values,\n instructions: prompt\n }).catch(() => undefined);\n };\n\n if (!dataEnhancementController?.enabled)\n return null;\n\n function submit() {\n generate(instructions);\n }\n\n return (\n <>\n <Menu\n align={\"end\"}\n sideOffset={8}\n className={\"max-w-[100vw]\"}\n // Never full width: this used to stretch to fill the form's\n // `w-80 2xl:w-96` side rail in full screen. That rail is gone, and\n // in the footer a stretched button reads as the primary action.\n trigger={<Button variant={\"filled\"}\n color={\"neutral\"}\n size={\"small\"}\n disabled={loading}>\n {!loading && <AIIcon size={\"small\"}/>}\n {loading && <CircularProgress size={\"small\"}/>}\n Autofill\n </Button>}>\n\n <MenuItem className={\"py-4\"}\n onClick={() => {\n generate();\n }}>\n <AIIcon size={\"small\"}/>\n Autofill based on the current content\n </MenuItem>\n\n <Separator orientation={\"horizontal\"} className={\"mt-2\"}/>\n\n {samplePrompts?.map((samplePrompt, index) => {\n return <MenuItem\n key={index + \"_\" + samplePrompt.prompt}\n onClick={() => {\n setInstructions(samplePrompt.prompt);\n generate(samplePrompt.prompt);\n }}\n >\n <div className={\"pl-9 grow text-text-secondary dark:text-text-secondary-dark\"}>\n {samplePrompt.prompt}\n </div>\n\n {samplePrompt.type === \"recent\" && <IconButton\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n removeRecentPrompt(storageKey, samplePrompt.prompt);\n setSamplePrompts((samplePrompts ?? []).filter(p => p.prompt !== samplePrompt.prompt));\n }}\n size={\"smallest\"}\n >\n <XIcon size={iconSize.smallest}/>\n </IconButton>\n }\n </MenuItem>;\n })}\n\n <Separator orientation={\"horizontal\"}/>\n\n {/* `px-4` and `gap-4` are MenuItem's own paddings, so the input\n row lines up with the items above it instead of sitting 8px\n to their left — which is what `mx-2` on the textarea did. */}\n {/* `items-center` so the send button sits on the field's centre\n line rather than pinned to its top edge as the textarea grows. */}\n <div\n className={cls(\n \"my-2 px-4 py-2 gap-4 w-[500px] max-w-full flex items-center text-surface-700 dark:text-surface-200\"\n )}>\n\n <div className={\"relative w-full grow\"}>\n {/* `fieldBackgroundMixin`, the same surface every other input\n in the codebase uses. It was `dark:bg-surface-950`, which\n theme.css defines as literal `#000000` — a pure black\n rectangle inside an already-dark menu. */}\n <TextareaAutosize\n className={cls(\"p-3 pr-12 rounded-lg resize-none w-full outline-hidden max-h-[300px] overflow-auto\", fieldBackgroundMixin, focusedDisabled)}\n value={instructions}\n autoFocus={status === \"new\"}\n disabled={loading}\n onFocus={(event) => {\n event.stopPropagation();\n }}\n placeholder={\"...or provide instructions\"}\n onKeyDown={(e) => {\n e.stopPropagation();\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n submit();\n }\n\n }}\n onChange={(e) => {\n setInstructions(e.target.value);\n }}\n />\n\n {/* Inside the field and only when there is something to\n clear — a permanently-visible X on an empty box is a\n control that does nothing. Positioned exactly as\n `TextFieldBinding` positions its own clearable X, so\n it stays centred as the textarea grows. */}\n {instructions.length > 0 && !loading && (\n <div\n className={\"flex flex-row justify-center items-center absolute h-full right-0 top-0 mr-2\"}>\n <IconButton\n size={\"small\"}\n onClick={() => {\n setInstructions(\"\");\n }}>\n <XIcon size={iconSize.small}/>\n </IconButton>\n </div>\n )}\n </div>\n\n <IconButton\n onClick={() => generate(instructions)}\n size={\"small\"}\n color={!instructions ? \"primary\" : undefined}\n disabled={loading || !instructions}>\n {loading &&\n <CircularProgress size={\"smallest\"}/>}\n {/* Sized, and no `color`. These icons are re-exported\n straight from lucide, so `color` lands on the SVG as\n a CSS colour — and `\"primary\"` is not one, which is\n why this button rendered empty. Every other icon in\n the codebase passes `size` alone and inherits\n `currentColor` from the button. */}\n {!loading &&\n <SendIcon size={iconSize.small}/>}\n </IconButton>\n\n </div>\n\n </Menu>\n\n <AutofillReviewDialog/>\n </>\n );\n}\n\nfunction getPromptsForExistingEntities(properties: Properties): SamplePrompt[] {\n\n const multilineProperties = Object.values(properties).filter((p: Property) => {\n if (isPropertyBuilder(p)) {\n return false;\n }\n return p.type === \"string\" && (p.admin?.markdown || p.admin?.multiline);\n });\n\n const multilinePrompt: Property | undefined = multilineProperties.length > 0\n ? multilineProperties[Math.floor(Math.random() * multilineProperties.length)] as Property\n : undefined;\n\n const prompts = [\n \"Fill the missing fields\",\n \"Translate the missing content\"\n ];\n if (multilinePrompt) {\n prompts.push(`Add 2 paragraphs to '${multilinePrompt.name}'`);\n }\n return prompts.map(p => ({\n prompt: p,\n type: \"sample\"\n }));\n}\n\nconst createLocalStorageKey = (path: string, status: EntityStatus) => {\n const statusString = status === \"new\" ? \"new\" : \"existing\";\n return `data_enhancement::${statusString}::${stripCollectionPath(path)}`;\n};\n\nconst getRecentPromptsFromStorage = (storageKey: string): SamplePrompt[] => {\n const item = localStorage.getItem(storageKey);\n return item ? JSON.parse(item).map((e: string) => ({\n prompt: e,\n type: \"recent\"\n })) : [];\n};\n\nconst addRecentPrompt = (storageKey: string, prompt: string) => {\n if (!prompt || prompt.trim().length === 0) {\n return;\n }\n const recentPrompts = getRecentPromptsFromStorage(storageKey);\n localStorage.setItem(storageKey, JSON.stringify([prompt, ...recentPrompts\n .map(e => e.prompt)\n .filter(e => e !== prompt)\n .slice(0, 5)]));\n};\n\nconst removeRecentPrompt = (storageKey: string, prompt: string) => {\n localStorage.setItem(storageKey, JSON.stringify(getRecentPromptsFromStorage(storageKey)\n .map(e => e.prompt)\n .filter(e => e !== prompt)));\n};\n","import React from \"react\";\n\nimport { CollectionConfig, User } from \"@rebasepro/types\";\nimport { RebasePlugin } from \"@rebasepro/admin-types\";\nimport { DataEnhancementControllerProvider } from \"./components/DataEnhancementControllerProvider\";\nimport { FormEnhanceAction } from \"./components/FormEnhanceAction\";\n\nexport interface DataEnhancementPluginProps {\n\n /**\n * Use this function to determine if the data enhancement plugin should be enabled for a given path.\n * If this function is not provided, the plugin will be enabled for all paths.\n * If the function returns false, the plugin will be disabled for the given path.\n *\n * @param path\n * @param collection\n */\n getConfigForPath?: (props: {\n path: string,\n collection: CollectionConfig,\n user: User | null\n }) => boolean;\n\n /**\n * Base URL of the AI service.\n *\n * Defaults to the one Rebase hosts, which is free to use and needs no\n * configuration. Point it at your own deployment to keep generation inside\n * your infrastructure — the wire format is documented in `src/api.ts`, and\n * the reference implementation is `saas/backend/functions/ai.ts`.\n *\n * Whatever it points at, the plugin renders nothing until that host's\n * `GET /status` reports itself available.\n */\n endpoint?: string;\n}\n\n/**\n * Use this hook to initialise the data enhancement plugin.\n * This is likely the only hook you will need to use.\n * @param props\n */\nexport function useDataEnhancementPlugin(props?: DataEnhancementPluginProps): RebasePlugin {\n\n const getConfigForPath = props?.getConfigForPath;\n const endpoint = props?.endpoint;\n\n return React.useMemo(() => ({\n key: \"data_enhancement\",\n slots: [\n {\n slot: \"form.actions\",\n Component: FormEnhanceAction,\n order: 40\n }\n ],\n providers: [\n {\n scope: \"form\" as const,\n Component: DataEnhancementControllerProvider as React.ComponentType<any>,\n props: {\n getConfigForPath,\n endpoint\n }\n }\n ]\n }), [getConfigForPath, endpoint]);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/plugin-ai",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.es.js",
|
|
7
7
|
"module": "./dist/index.es.js",
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
"./package.json": "./package.json"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@rebasepro/admin": "0.
|
|
20
|
-
"@rebasepro/admin-types": "0.
|
|
21
|
-
"@rebasepro/app": "0.
|
|
22
|
-
"@rebasepro/
|
|
23
|
-
"@rebasepro/
|
|
24
|
-
"@rebasepro/ui": "0.
|
|
25
|
-
"@rebasepro/utils": "0.
|
|
19
|
+
"@rebasepro/admin": "0.13.0",
|
|
20
|
+
"@rebasepro/admin-types": "0.13.0",
|
|
21
|
+
"@rebasepro/app": "0.13.0",
|
|
22
|
+
"@rebasepro/common": "0.13.0",
|
|
23
|
+
"@rebasepro/types": "0.13.0",
|
|
24
|
+
"@rebasepro/ui": "0.13.0",
|
|
25
|
+
"@rebasepro/utils": "0.13.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=19.2.7",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@testing-library/jest-dom": "^7.0.0",
|
|
46
46
|
"@testing-library/react": "^16.3.2",
|
|
47
|
+
"@testing-library/user-event": "^14.6.1",
|
|
47
48
|
"@types/jest": "^30.0.0",
|
|
48
49
|
"@types/node": "^26.1.2",
|
|
49
50
|
"@types/react": "^19.2.17",
|
package/src/api.ts
CHANGED
|
@@ -233,6 +233,8 @@ export async function autocompleteStream(props: {
|
|
|
233
233
|
*/
|
|
234
234
|
export async function fetchPromptSuggestions(props: {
|
|
235
235
|
entityName: string;
|
|
236
|
+
/** Ties suggestions to the domain — see the note on the service's side. */
|
|
237
|
+
entityDescription?: string;
|
|
236
238
|
input?: string;
|
|
237
239
|
endpoint?: string;
|
|
238
240
|
signal?: AbortSignal;
|
|
@@ -243,6 +245,7 @@ export async function fetchPromptSuggestions(props: {
|
|
|
243
245
|
headers: { "Content-Type": "application/json" },
|
|
244
246
|
body: JSON.stringify({
|
|
245
247
|
entityName: props.entityName,
|
|
248
|
+
entityDescription: props.entityDescription,
|
|
246
249
|
input: props.input
|
|
247
250
|
}),
|
|
248
251
|
signal: props.signal
|
|
@@ -69,7 +69,12 @@ export function AutofillReviewDialog() {
|
|
|
69
69
|
size={"small"}
|
|
70
70
|
onCheckedChange={() => controller.toggleAll(!allSelected)}
|
|
71
71
|
/>
|
|
72
|
-
|
|
72
|
+
{/* `component="span"`: the Typography `label`
|
|
73
|
+
variant renders a <label> element, and this sits
|
|
74
|
+
inside the row's own <label>. Nested labels are
|
|
75
|
+
invalid HTML and stop the text toggling the
|
|
76
|
+
checkbox — clicking "Select all" did nothing. */}
|
|
77
|
+
<Typography variant={"label"} component={"span"} color={"secondary"}>
|
|
73
78
|
{allSelected ? "Deselect all" : "Select all"}
|
|
74
79
|
</Typography>
|
|
75
80
|
</label>
|
|
@@ -150,7 +155,8 @@ function ProposedFieldRow({ field, onToggle }: { field: ProposedField, onToggle:
|
|
|
150
155
|
|
|
151
156
|
<div className={"flex flex-col gap-1 min-w-0 grow"}>
|
|
152
157
|
<div className={"flex items-center gap-2"}>
|
|
153
|
-
|
|
158
|
+
{/* See the note above: never a bare `label` variant inside a <label>. */}
|
|
159
|
+
<Typography variant={"label"} component={"span"}>{field.label}</Typography>
|
|
154
160
|
{replaces && (
|
|
155
161
|
<Typography variant={"caption"} color={"secondary"}>
|
|
156
162
|
replaces the current value
|
|
@@ -187,8 +187,13 @@ proposed: String(existing.proposed ?? "") + text }
|
|
|
187
187
|
setReview((current) => current && {
|
|
188
188
|
...current,
|
|
189
189
|
status: "ready",
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
// Fields still pending when the run ended never received a final
|
|
191
|
+
// value — the model's JSON was cut off mid-string, so all we
|
|
192
|
+
// hold is a half-written sentence. Marking them complete would
|
|
193
|
+
// make that sentence applicable, which is the exact outcome the
|
|
194
|
+
// review exists to prevent. They are dropped instead: the review
|
|
195
|
+
// only ever offers what the service actually finished.
|
|
196
|
+
fields: current.fields.filter((f) => !f.pending)
|
|
192
197
|
});
|
|
193
198
|
} catch (e: unknown) {
|
|
194
199
|
const message = e instanceof Error ? e.message : "Autofill could not be completed";
|
|
@@ -199,8 +204,9 @@ pending: false }))
|
|
|
199
204
|
...current,
|
|
200
205
|
status: "failed",
|
|
201
206
|
error: message,
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
// Same rule as the success path: a field interrupted mid-write
|
|
208
|
+
// is not something the operator can be offered.
|
|
209
|
+
fields: current.fields.filter((f) => !f.pending)
|
|
204
210
|
});
|
|
205
211
|
}
|
|
206
212
|
}, [collection, endpoint, upsertField]);
|
|
@@ -238,10 +244,13 @@ selected }))
|
|
|
238
244
|
const editorAIController = useEditorAIController({ endpoint });
|
|
239
245
|
|
|
240
246
|
const getSamplePrompts = useCallback(
|
|
241
|
-
(entityName: string, input?: string) => fetchPromptSuggestions({
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
247
|
+
(entityName: string, input?: string) => fetchPromptSuggestions({
|
|
248
|
+
endpoint,
|
|
249
|
+
entityName,
|
|
250
|
+
entityDescription: collection.description,
|
|
251
|
+
input
|
|
252
|
+
}),
|
|
253
|
+
[endpoint, collection.description]
|
|
245
254
|
);
|
|
246
255
|
|
|
247
256
|
const dataEnhancementController: DataEnhancementController = useMemo(() => ({
|