@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.
- package/README.md +60 -0
- package/dist/ai/promptCaching.d.ts +23 -0
- package/dist/ai/promptCaching.js +120 -0
- package/dist/ai/proposalRepair.d.ts +30 -0
- package/dist/ai/proposalRepair.js +76 -0
- package/dist/ai/sensitiveData.d.ts +1 -0
- package/dist/ai/sensitiveData.js +3 -1
- package/dist/components/ai-input/AIInput.js +35 -5
- package/dist/components/ai-input/AIInput.module.css +49 -0
- package/dist/components/generate-field/GenerateField.d.ts +11 -0
- package/dist/components/generate-field/GenerateField.js +109 -0
- package/dist/components/generate-field/GenerateField.module.css +39 -0
- package/dist/components/generate-field/richText.d.ts +29 -0
- package/dist/components/generate-field/richText.js +30 -0
- package/dist/components/translate-document/TranslateDocumentButton.d.ts +2 -0
- package/dist/components/translate-document/TranslateDocumentButton.js +111 -0
- package/dist/components/translate-document/TranslateDocumentButton.module.css +11 -0
- package/dist/exports/client.d.ts +4 -0
- package/dist/exports/client.js +4 -0
- package/dist/handlers/chatHandler.d.ts +1 -0
- package/dist/handlers/chatHandler.js +464 -138
- package/dist/handlers/generateFieldHandler.d.ts +14 -0
- package/dist/handlers/generateFieldHandler.js +144 -0
- package/dist/handlers/translateDocumentHandler.d.ts +14 -0
- package/dist/handlers/translateDocumentHandler.js +186 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +112 -4
- package/dist/payload/documentTranslation.d.ts +15 -0
- package/dist/payload/documentTranslation.js +120 -0
- package/dist/payload/textFieldGeneration.d.ts +26 -0
- package/dist/payload/textFieldGeneration.js +99 -0
- package/dist/payload/toolFieldSelection.d.ts +25 -0
- package/dist/payload/toolFieldSelection.js +118 -0
- package/dist/payload/toolSchemas.d.ts +7 -0
- package/dist/payload/toolSchemas.js +117 -0
- 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
|
+
};
|
package/dist/ai/sensitiveData.js
CHANGED
|
@@ -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:
|
|
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(
|
|
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 {};
|