@mvriu5/payload-ai 1.4.0 → 1.6.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.
Files changed (36) hide show
  1. package/README.md +60 -0
  2. package/dist/ai/promptCaching.d.ts +23 -0
  3. package/dist/ai/promptCaching.js +120 -0
  4. package/dist/ai/proposalRepair.d.ts +30 -0
  5. package/dist/ai/proposalRepair.js +76 -0
  6. package/dist/ai/sensitiveData.d.ts +1 -0
  7. package/dist/ai/sensitiveData.js +3 -1
  8. package/dist/components/ai-input/AIInput.js +35 -5
  9. package/dist/components/ai-input/AIInput.module.css +49 -0
  10. package/dist/components/generate-field/GenerateField.d.ts +11 -0
  11. package/dist/components/generate-field/GenerateField.js +109 -0
  12. package/dist/components/generate-field/GenerateField.module.css +39 -0
  13. package/dist/components/generate-field/richText.d.ts +29 -0
  14. package/dist/components/generate-field/richText.js +30 -0
  15. package/dist/components/translate-document/TranslateDocumentButton.d.ts +2 -0
  16. package/dist/components/translate-document/TranslateDocumentButton.js +111 -0
  17. package/dist/components/translate-document/TranslateDocumentButton.module.css +11 -0
  18. package/dist/exports/client.d.ts +4 -0
  19. package/dist/exports/client.js +4 -0
  20. package/dist/handlers/chatHandler.d.ts +1 -0
  21. package/dist/handlers/chatHandler.js +464 -138
  22. package/dist/handlers/generateFieldHandler.d.ts +14 -0
  23. package/dist/handlers/generateFieldHandler.js +144 -0
  24. package/dist/handlers/translateDocumentHandler.d.ts +14 -0
  25. package/dist/handlers/translateDocumentHandler.js +186 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.js +112 -4
  28. package/dist/payload/documentTranslation.d.ts +15 -0
  29. package/dist/payload/documentTranslation.js +120 -0
  30. package/dist/payload/textFieldGeneration.d.ts +26 -0
  31. package/dist/payload/textFieldGeneration.js +99 -0
  32. package/dist/payload/toolFieldSelection.d.ts +25 -0
  33. package/dist/payload/toolFieldSelection.js +118 -0
  34. package/dist/payload/toolSchemas.d.ts +7 -0
  35. package/dist/payload/toolSchemas.js +117 -0
  36. package/package.json +3 -3
package/README.md CHANGED
@@ -54,6 +54,7 @@ When `providers` is configured, provider selection and API keys are managed cent
54
54
  import type { PayloadAIPluginOptions } from "@mvriu5/payload-ai";
55
55
 
56
56
  const options: PayloadAIPluginOptions = {
57
+ aiInput: true,
57
58
  allowUserApiKeys: false,
58
59
  collections: {
59
60
  media: {
@@ -68,6 +69,7 @@ const options: PayloadAIPluginOptions = {
68
69
  },
69
70
  users: true,
70
71
  },
72
+ generateFields: true,
71
73
  media: {
72
74
  enabled: true,
73
75
  collectionSlug: "media",
@@ -99,6 +101,8 @@ const options: PayloadAIPluginOptions = {
99
101
  },
100
102
  ],
101
103
  maxOutputTokens: 1200,
104
+ promptCaching: true,
105
+ translate: true,
102
106
  maxTokenUsage: {
103
107
  type: "user",
104
108
  perDay: 50_000,
@@ -109,6 +113,50 @@ const options: PayloadAIPluginOptions = {
109
113
 
110
114
  `models` configures model choices for the user-selected provider mode. Use `providers` instead when provider selection, credentials, and endpoints should be managed centrally.
111
115
 
116
+ ### `aiInput`
117
+
118
+ Controls whether the collapsible AI Assistant input is added to collection and global edit views. It defaults to `true`.
119
+
120
+ ```ts
121
+ payloadAiPlugin({
122
+ aiInput: false,
123
+ })
124
+ ```
125
+
126
+ Disabling this option does not remove the AI dashboard or per-field Generate controls.
127
+
128
+ ### `generateFields`
129
+
130
+ Controls whether supported fields receive a Generate control. It defaults to `true`.
131
+
132
+ ```ts
133
+ payloadAiPlugin({
134
+ generateFields: false,
135
+ })
136
+ ```
137
+
138
+ Generate controls are added to `text`, `textarea`, `richText`, and `json` fields in collection and global edit views. When disabled, the plugin also omits the `/ai-generate-field` endpoint. The embedded AI Assistant and dashboard remain available.
139
+
140
+ ### `translate`
141
+
142
+ Controls document translation for localized collections and globals. It defaults to `true`.
143
+
144
+ ```ts
145
+ payloadAiPlugin({
146
+ translate: false,
147
+ })
148
+ ```
149
+
150
+ When enabled, the plugin adds a `Translate` action to the document header. It is shown only when:
151
+
152
+ - a non-default locale is selected
153
+ - at least one localized field is empty in that locale
154
+ - the corresponding field in the default locale contains source content
155
+
156
+ Translation fills only empty localized fields in the current Payload form and marks it as modified. Existing translated values are never overwritten. It does not save the document or global, so the generated values can be reviewed and validated before saving.
157
+
158
+ When disabled, the plugin does not register the header control or the `/ai-translate-document` endpoint.
159
+
112
160
  ### `collections`
113
161
 
114
162
  Restricts AI read and write proposals to enabled collection slugs. If omitted, all non-internal Payload collections are available.
@@ -250,6 +298,18 @@ Use `type: "user"` to enforce separate budgets per authenticated user, or `type:
250
298
 
251
299
  Completed model usage is stored in the hidden `payload-ai-usage` collection. Requests made after a limit is reached return HTTP `429`. Because providers report token usage after completion, the request that crosses a limit is allowed to finish and subsequent requests are blocked.
252
300
 
301
+ ### `promptCaching`
302
+
303
+ Enables provider prompt-caching hints and defaults to `true`. The chat endpoint keeps static instructions and Payload schemas in a stable prompt prefix, adds Anthropic cache-control breakpoints, and supplies a stable OpenAI prompt cache key. Gemini uses the same stable prefix with its implicit caching.
304
+
305
+ ```ts
306
+ payloadAiPlugin({
307
+ promptCaching: false,
308
+ })
309
+ ```
310
+
311
+ Disable it when explicit provider-side caching is not desired. Dynamic document data and user prompts are never placed in the cacheable schema section. Managed providers with a custom `baseURL` receive the stable prefix but no provider-specific cache parameters, preserving compatibility with Ollama, vLLM, and other OpenAI-compatible endpoints.
312
+
253
313
  ### `allowUserApiKeys`
254
314
 
255
315
  Controls whether the plugin adds an `aiApiKey` field to the admin user collection.
@@ -0,0 +1,23 @@
1
+ import type { SystemModelMessage } from "ai";
2
+ import type { AIProvider } from "./providerOptions.js";
3
+ type PromptCacheContext = {
4
+ cacheable: Record<string, unknown>[];
5
+ dynamic: Record<string, unknown>[];
6
+ };
7
+ type PromptCacheProviderOptions = NonNullable<SystemModelMessage["providerOptions"]>;
8
+ export declare const splitPromptCacheContext: (mentionContext: Record<string, unknown>[]) => PromptCacheContext;
9
+ export declare const createCachedSystemMessages: ({ cacheableContext, cacheableInstructions, dynamicInstructions, enabled, provider, staticInstructions, }: {
10
+ cacheableContext: Record<string, unknown>[];
11
+ cacheableInstructions: string[];
12
+ dynamicInstructions: string[];
13
+ enabled: boolean;
14
+ provider: AIProvider;
15
+ staticInstructions: string[];
16
+ }) => SystemModelMessage[];
17
+ export declare const createPromptCacheProviderOptions: ({ cacheKeyParts, enabled, model, provider, }: {
18
+ cacheKeyParts: unknown[];
19
+ enabled: boolean;
20
+ model: string;
21
+ provider: AIProvider;
22
+ }) => PromptCacheProviderOptions | undefined;
23
+ export {};
@@ -0,0 +1,120 @@
1
+ import { createHash } from "node:crypto";
2
+ const getContextKey = (context)=>{
3
+ return [
4
+ context.type,
5
+ context.parent,
6
+ context.collection,
7
+ context.slug
8
+ ].filter(Boolean).join(":") || JSON.stringify(context);
9
+ };
10
+ export const splitPromptCacheContext = (mentionContext)=>{
11
+ const cacheable = [];
12
+ const dynamic = [];
13
+ const cacheableKeys = new Set();
14
+ const addCacheable = (context)=>{
15
+ const key = getContextKey(context);
16
+ if (cacheableKeys.has(key)) return;
17
+ cacheableKeys.add(key);
18
+ cacheable.push(context);
19
+ };
20
+ for (const context of mentionContext){
21
+ if ([
22
+ "block",
23
+ "collection"
24
+ ].includes(String(context.type))) {
25
+ addCacheable(context);
26
+ continue;
27
+ }
28
+ if (context.type === "global" && "doc" in context) {
29
+ const { doc, ...schema } = context;
30
+ addCacheable(schema);
31
+ dynamic.push({
32
+ doc,
33
+ slug: context.slug,
34
+ type: "globalDocument"
35
+ });
36
+ continue;
37
+ }
38
+ if (context.type === "global") {
39
+ addCacheable(context);
40
+ continue;
41
+ }
42
+ if (context.type === "mediaAttachment" && context.schema && typeof context.schema === "object") {
43
+ addCacheable(context.schema);
44
+ const { schema: _schema, ...attachmentContext } = context;
45
+ dynamic.push(attachmentContext);
46
+ continue;
47
+ }
48
+ dynamic.push(context);
49
+ }
50
+ return {
51
+ cacheable,
52
+ dynamic
53
+ };
54
+ };
55
+ const withAnthropicCacheControl = (message, enabled, provider)=>{
56
+ if (!enabled || provider !== "claude") return message;
57
+ return {
58
+ ...message,
59
+ providerOptions: {
60
+ anthropic: {
61
+ cacheControl: {
62
+ type: "ephemeral"
63
+ }
64
+ }
65
+ }
66
+ };
67
+ };
68
+ export const createCachedSystemMessages = ({ cacheableContext, cacheableInstructions, dynamicInstructions, enabled, provider, staticInstructions })=>{
69
+ const messages = [
70
+ withAnthropicCacheControl({
71
+ content: staticInstructions.join("\n"),
72
+ role: "system"
73
+ }, enabled, provider)
74
+ ];
75
+ const scopedContent = [
76
+ ...cacheableContext.length ? [
77
+ "Payload schema references JSON. Use exact field names and values:",
78
+ JSON.stringify(cacheableContext)
79
+ ] : [],
80
+ ...cacheableInstructions
81
+ ];
82
+ if (scopedContent.length > 0) {
83
+ messages.push(withAnthropicCacheControl({
84
+ content: scopedContent.join("\n"),
85
+ role: "system"
86
+ }, enabled, provider));
87
+ }
88
+ if (dynamicInstructions.length > 0) {
89
+ messages.push({
90
+ content: dynamicInstructions.join("\n"),
91
+ role: "system"
92
+ });
93
+ }
94
+ return messages;
95
+ };
96
+ export const createPromptCacheProviderOptions = ({ cacheKeyParts, enabled, model, provider })=>{
97
+ if (!enabled) return undefined;
98
+ if (provider === "openai") {
99
+ const digest = createHash("sha256").update(JSON.stringify([
100
+ "payload-ai-v1",
101
+ model,
102
+ ...cacheKeyParts
103
+ ])).digest("hex").slice(0, 32);
104
+ return {
105
+ openai: {
106
+ promptCacheKey: `payload-ai-${digest}`
107
+ }
108
+ };
109
+ }
110
+ if (provider === "openrouter" && model.toLowerCase().startsWith("anthropic/")) {
111
+ return {
112
+ openrouter: {
113
+ cache_control: {
114
+ type: "ephemeral"
115
+ }
116
+ }
117
+ };
118
+ }
119
+ return undefined;
120
+ };
@@ -0,0 +1,30 @@
1
+ import type { ProposalValidationIssue } from "../payload/proposalData.js";
2
+ export declare const maxProposalRepairAttempts = 1;
3
+ export type CompactProposalRepairIssue = {
4
+ code: string;
5
+ hint: string;
6
+ path: string;
7
+ };
8
+ type ProposalRepairTarget = {
9
+ collection?: string;
10
+ id?: string;
11
+ slug?: string;
12
+ tool: string;
13
+ };
14
+ export declare const compactProposalRepairIssues: (issues: ProposalValidationIssue[]) => CompactProposalRepairIssue[];
15
+ export declare const createProposalRepairKey: ({ collection, id, slug, tool }: ProposalRepairTarget) => string;
16
+ export declare const createProposalRepairTracker: () => {
17
+ beginCall(target: ProposalRepairTarget): "initial" | "blocked" | "repair";
18
+ registerFailure(target: ProposalRepairTarget): {
19
+ attempt: number;
20
+ errorCode: "REPAIR_EXHAUSTED";
21
+ maxAttempts: number;
22
+ retryable: boolean;
23
+ } | {
24
+ attempt: number;
25
+ errorCode: "INVALID_PROPOSAL_DATA";
26
+ maxAttempts: number;
27
+ retryable: boolean;
28
+ };
29
+ };
30
+ export {};
@@ -0,0 +1,76 @@
1
+ export const maxProposalRepairAttempts = 1;
2
+ const getIssueHint = (issue)=>{
3
+ switch(issue.code){
4
+ case "invalid_array":
5
+ return "Use an array of complete objects matching the child fields.";
6
+ case "invalid_block_type":
7
+ return "Use an exact blockType from the schema.";
8
+ case "invalid_blocks":
9
+ return "Use an array of block objects with blockType and exact field names.";
10
+ case "invalid_checkbox":
11
+ return "Use true or false.";
12
+ case "invalid_container":
13
+ return "Use an object matching the nested field schema.";
14
+ case "invalid_date":
15
+ return "Use a valid date string.";
16
+ case "invalid_option":
17
+ return issue.message;
18
+ case "invalid_relationship":
19
+ return "Use an existing document ID or { relationTo, value } for polymorphic relationships.";
20
+ case "missing_required_field":
21
+ return "Add the required field with a schema-compatible value.";
22
+ case "non_localized_field_in_secondary_locale":
23
+ return "Move this field to the primary locale data.";
24
+ case "unknown_field":
25
+ return "Remove it and use exact schema field names only.";
26
+ default:
27
+ return issue.message;
28
+ }
29
+ };
30
+ export const compactProposalRepairIssues = (issues)=>{
31
+ return issues.slice(0, 6).map((issue)=>({
32
+ code: issue.code,
33
+ hint: getIssueHint(issue).slice(0, 180),
34
+ path: issue.path
35
+ }));
36
+ };
37
+ export const createProposalRepairKey = ({ collection, id, slug, tool })=>{
38
+ return [
39
+ tool,
40
+ collection || slug || "",
41
+ id || ""
42
+ ].join(":");
43
+ };
44
+ export const createProposalRepairTracker = ()=>{
45
+ const statesByTarget = new Map();
46
+ return {
47
+ beginCall (target) {
48
+ const state = statesByTarget.get(createProposalRepairKey(target));
49
+ if (!state) return "initial";
50
+ if (state.repairCallConsumed) return "blocked";
51
+ state.repairCallConsumed = true;
52
+ return "repair";
53
+ },
54
+ registerFailure (target) {
55
+ const key = createProposalRepairKey(target);
56
+ const state = statesByTarget.get(key);
57
+ if (state) {
58
+ return {
59
+ attempt: maxProposalRepairAttempts,
60
+ errorCode: "REPAIR_EXHAUSTED",
61
+ maxAttempts: maxProposalRepairAttempts,
62
+ retryable: false
63
+ };
64
+ }
65
+ statesByTarget.set(key, {
66
+ repairCallConsumed: false
67
+ });
68
+ return {
69
+ attempt: 1,
70
+ errorCode: "INVALID_PROPOSAL_DATA",
71
+ maxAttempts: maxProposalRepairAttempts,
72
+ retryable: true
73
+ };
74
+ }
75
+ };
76
+ };
@@ -1,2 +1,3 @@
1
+ export declare const isSensitiveKey: (key: string) => boolean;
1
2
  export declare const containsSensitiveData: (value: unknown) => boolean;
2
3
  export declare const redactSensitiveData: (value: unknown) => unknown;
@@ -5,10 +5,12 @@ const sensitiveKeyPatterns = [
5
5
  /^aiApiKey$/i,
6
6
  /^authorization$/i,
7
7
  /^accessToken$/i,
8
+ /^password$/i,
9
+ /^privateKey$/i,
8
10
  /^refreshToken$/i,
9
11
  /^secret$/i
10
12
  ];
11
- const isSensitiveKey = (key)=>sensitiveKeyPatterns.some((pattern)=>pattern.test(key));
13
+ export const isSensitiveKey = (key)=>sensitiveKeyPatterns.some((pattern)=>pattern.test(key));
12
14
  export const containsSensitiveData = (value)=>{
13
15
  if (Array.isArray(value)) return value.some(containsSensitiveData);
14
16
  if (!isRecord(value)) return false;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Button, useConfig, useDocumentForm, useDocumentInfo, useLocale } from "@payloadcms/ui";
3
+ import { Button, ChevronIcon, useConfig, useDocumentForm, useDocumentInfo, useLocale } from "@payloadcms/ui";
4
4
  import { formatAdminURL } from "payload/shared";
5
5
  import { useEffect, useRef, useState } from "react";
6
6
  import { mergeData } from "../../payload/shared.js";
@@ -281,7 +281,7 @@ const AIInputCore = ({ applyProposalLocally, documentScope, isDashboard })=>{
281
281
  children: /*#__PURE__*/ _jsxs("div", {
282
282
  className: styles.chat,
283
283
  children: [
284
- /*#__PURE__*/ _jsx("div", {
284
+ isDashboard && /*#__PURE__*/ _jsx("div", {
285
285
  className: styles.chatHeader,
286
286
  children: /*#__PURE__*/ _jsx("h2", {
287
287
  className: styles.chatTitle,
@@ -299,13 +299,13 @@ const AIInputCore = ({ applyProposalLocally, documentScope, isDashboard })=>{
299
299
  return /*#__PURE__*/ _jsx("div", {
300
300
  className: styles.chatLayout,
301
301
  style: {
302
- marginBottom: isDashboard ? "0" : "20px",
302
+ marginBottom: "0",
303
303
  height: isDashboard ? "332px" : "220px"
304
304
  },
305
305
  children: /*#__PURE__*/ _jsxs("div", {
306
306
  className: styles.chat,
307
307
  children: [
308
- /*#__PURE__*/ _jsx("div", {
308
+ isDashboard && /*#__PURE__*/ _jsx("div", {
309
309
  className: styles.chatHeader,
310
310
  children: /*#__PURE__*/ _jsx("h2", {
311
311
  className: styles.chatTitle,
@@ -543,6 +543,36 @@ const AIInput = ({ isDashboard = false })=>{
543
543
  if (isDashboard) return /*#__PURE__*/ _jsx(AIInputCore, {
544
544
  isDashboard: true
545
545
  });
546
- return /*#__PURE__*/ _jsx(DocumentAIInput, {});
546
+ return /*#__PURE__*/ _jsx(CollapsibleDocumentAIInput, {});
547
+ };
548
+ const CollapsibleDocumentAIInput = ()=>{
549
+ const [isExpanded, setIsExpanded] = useState(false);
550
+ return /*#__PURE__*/ _jsxs("div", {
551
+ className: styles.collapsible,
552
+ children: [
553
+ /*#__PURE__*/ _jsxs("button", {
554
+ "aria-controls": "payload-ai-input-content",
555
+ "aria-expanded": isExpanded,
556
+ className: styles.collapsibleToggle,
557
+ onClick: ()=>setIsExpanded((current)=>!current),
558
+ type: "button",
559
+ children: [
560
+ /*#__PURE__*/ _jsx("span", {
561
+ children: "AI Assistant"
562
+ }),
563
+ /*#__PURE__*/ _jsx(ChevronIcon, {
564
+ ariaLabel: isExpanded ? "Collapse AI Assistant" : "Expand AI Assistant",
565
+ className: styles.collapsibleChevron,
566
+ direction: isExpanded ? "up" : "down"
567
+ })
568
+ ]
569
+ }),
570
+ isExpanded && /*#__PURE__*/ _jsx("div", {
571
+ className: styles.collapsibleContent,
572
+ id: "payload-ai-input-content",
573
+ children: /*#__PURE__*/ _jsx(DocumentAIInput, {})
574
+ })
575
+ ]
576
+ });
547
577
  };
548
578
  export default AIInput;
@@ -1,3 +1,52 @@
1
+ .collapsible {
2
+ border: 1px solid var(--theme-elevation-150);
3
+ border-radius: 8px;
4
+ margin-bottom: 20px;
5
+ overflow: hidden;
6
+ width: 100%;
7
+ }
8
+
9
+ .collapsibleToggle {
10
+ align-items: center;
11
+ background: transparent;
12
+ border: 0;
13
+ color: var(--theme-text);
14
+ cursor: pointer;
15
+ display: flex;
16
+ font: inherit;
17
+ font-weight: 600;
18
+ justify-content: space-between;
19
+ min-height: 44px;
20
+ padding: 0 20px;
21
+ text-align: left;
22
+ width: 100%;
23
+ }
24
+
25
+ .collapsibleToggle:hover {
26
+ color: var(--theme-elevation-800);
27
+ }
28
+
29
+ .collapsibleToggle:focus-visible {
30
+ outline: 2px solid var(--theme-success-500);
31
+ outline-offset: 2px;
32
+ }
33
+
34
+ .collapsibleToggle .collapsibleChevron {
35
+ flex: 0 0 24px;
36
+ height: 24px;
37
+ width: 24px;
38
+ }
39
+
40
+ .collapsibleContent {
41
+ padding: 0;
42
+ }
43
+
44
+ .collapsibleContent .chat {
45
+ border: 0;
46
+ border-radius: 0;
47
+ padding-top: 0;
48
+ }
49
+
1
50
  .chatLayout {
2
51
  width: 100%;
3
52
  max-width: none;
@@ -0,0 +1,11 @@
1
+ type GenerateFieldProps = {
2
+ field?: {
3
+ hasMany?: boolean;
4
+ };
5
+ generationFieldKey: string;
6
+ generationFieldType: "json" | "richText" | "text" | "textarea";
7
+ path: string;
8
+ readOnly?: boolean;
9
+ };
10
+ declare const GenerateField: ({ field, generationFieldKey, generationFieldType, path, readOnly }: GenerateFieldProps) => import("react").JSX.Element | null;
11
+ export default GenerateField;
@@ -0,0 +1,109 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useConfig, useDocumentForm, useDocumentInfo, useField, useLocale } from "@payloadcms/ui";
4
+ import { formatAdminURL } from "payload/shared";
5
+ import { useState } from "react";
6
+ import { createGeneratedRichTextValue } from "./richText.js";
7
+ import styles from "./GenerateField.module.css";
8
+ const pendingRequests = new Map();
9
+ const requestGeneratedValue = ({ apiRoute, body, cacheKey })=>{
10
+ const pending = pendingRequests.get(cacheKey);
11
+ if (pending) return pending;
12
+ const request = fetch(formatAdminURL({
13
+ apiRoute,
14
+ path: "/ai-generate-field"
15
+ }), {
16
+ body: JSON.stringify(body),
17
+ headers: {
18
+ "Content-Type": "application/json"
19
+ },
20
+ method: "POST"
21
+ }).then(async (response)=>{
22
+ const result = await response.json().catch(()=>null);
23
+ if (!response.ok || result?.value === undefined) throw new Error(result?.error || "Could not generate field content.");
24
+ return result;
25
+ }).finally(()=>pendingRequests.delete(cacheKey));
26
+ pendingRequests.set(cacheKey, request);
27
+ return request;
28
+ };
29
+ const GenerateField = ({ field, generationFieldKey, generationFieldType, path, readOnly })=>{
30
+ const configContext = useConfig();
31
+ const documentForm = useDocumentForm();
32
+ const documentInfo = useDocumentInfo();
33
+ const locale = useLocale();
34
+ const { disabled, setValue } = useField({
35
+ path
36
+ });
37
+ const [error, setError] = useState("");
38
+ const [isGenerating, setIsGenerating] = useState(false);
39
+ const scope = documentInfo.collectionSlug ? {
40
+ slug: documentInfo.collectionSlug,
41
+ type: "collection"
42
+ } : documentInfo.globalSlug ? {
43
+ slug: documentInfo.globalSlug,
44
+ type: "global"
45
+ } : null;
46
+ const generate = async ()=>{
47
+ const apiRoute = configContext?.config?.routes?.api;
48
+ if (!apiRoute || !scope || isGenerating) return;
49
+ setError("");
50
+ setIsGenerating(true);
51
+ try {
52
+ const context = documentForm.getData();
53
+ const cacheKey = JSON.stringify([
54
+ scope.type,
55
+ scope.slug,
56
+ generationFieldKey,
57
+ locale.code,
58
+ context
59
+ ]);
60
+ const result = await requestGeneratedValue({
61
+ apiRoute,
62
+ body: {
63
+ context,
64
+ fieldKey: generationFieldKey,
65
+ locale: locale.code,
66
+ scope
67
+ },
68
+ cacheKey
69
+ });
70
+ if (generationFieldType === "richText") {
71
+ const value = createGeneratedRichTextValue(result.value);
72
+ documentForm.dispatchFields({
73
+ initialValue: value,
74
+ path,
75
+ type: "UPDATE",
76
+ value
77
+ });
78
+ } else {
79
+ setValue(field?.hasMany ? [
80
+ result.value
81
+ ] : result.value);
82
+ }
83
+ documentForm.setModified(true);
84
+ } catch (err) {
85
+ setError(err instanceof Error ? err.message : "Could not generate field content.");
86
+ } finally{
87
+ setIsGenerating(false);
88
+ }
89
+ };
90
+ if (!scope) return null;
91
+ return /*#__PURE__*/ _jsxs("div", {
92
+ className: styles.generateField,
93
+ children: [
94
+ /*#__PURE__*/ _jsx("button", {
95
+ className: styles.generateButton,
96
+ disabled: Boolean(readOnly || disabled || isGenerating),
97
+ onClick: ()=>void generate(),
98
+ type: "button",
99
+ children: isGenerating ? "Generating..." : "Generate"
100
+ }),
101
+ error && /*#__PURE__*/ _jsx("span", {
102
+ className: styles.generateError,
103
+ role: "alert",
104
+ children: error
105
+ })
106
+ ]
107
+ });
108
+ };
109
+ export default GenerateField;
@@ -0,0 +1,39 @@
1
+ .generateField {
2
+ align-items: center;
3
+ display: flex;
4
+ gap: 8px;
5
+ justify-content: flex-end;
6
+ margin-top: 6px;
7
+ }
8
+
9
+ .generateButton {
10
+ background: transparent;
11
+ border: 0;
12
+ color: var(--theme-elevation-600);
13
+ cursor: pointer;
14
+ font: inherit;
15
+ font-size: 12px;
16
+ line-height: 1.2;
17
+ padding: 2px 0;
18
+ }
19
+
20
+ .generateButton:hover,
21
+ .generateButton:focus-visible {
22
+ color: var(--theme-text);
23
+ }
24
+
25
+ .generateButton:focus-visible {
26
+ outline: 1px solid currentColor;
27
+ outline-offset: 2px;
28
+ }
29
+
30
+ .generateButton:disabled {
31
+ cursor: not-allowed;
32
+ opacity: 0.45;
33
+ }
34
+
35
+ .generateError {
36
+ color: var(--theme-error-500);
37
+ font-size: 11px;
38
+ line-height: 1.2;
39
+ }
@@ -0,0 +1,29 @@
1
+ type LexicalTextNode = {
2
+ detail: number;
3
+ format: number;
4
+ mode: "normal";
5
+ style: string;
6
+ text: string;
7
+ type: "text";
8
+ version: 1;
9
+ };
10
+ type LexicalParagraphNode = {
11
+ children: LexicalTextNode[];
12
+ direction: "ltr";
13
+ format: "";
14
+ indent: number;
15
+ type: "paragraph";
16
+ version: 1;
17
+ };
18
+ export type GeneratedRichTextValue = {
19
+ root: {
20
+ children: LexicalParagraphNode[];
21
+ direction: "ltr";
22
+ format: "";
23
+ indent: number;
24
+ type: "root";
25
+ version: 1;
26
+ };
27
+ };
28
+ export declare const createGeneratedRichTextValue: (value: string) => GeneratedRichTextValue;
29
+ export {};