@mvriu5/payload-ai 1.5.0 → 1.6.1
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 +161 -100
- 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.d.ts +1 -1
- package/dist/components/ai-input/AIInput.js +3 -1
- package/dist/components/generate-field/GenerateField.js +3 -2
- 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 +2 -0
- package/dist/exports/client.js +2 -0
- package/dist/handlers/chatHandler.d.ts +1 -0
- package/dist/handlers/chatHandler.js +464 -138
- package/dist/handlers/translateDocumentHandler.d.ts +14 -0
- package/dist/handlers/translateDocumentHandler.js +186 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +75 -6
- package/dist/payload/documentTranslation.d.ts +15 -0
- package/dist/payload/documentTranslation.js +120 -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 +2 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PayloadHandler } from "payload";
|
|
2
|
+
import { type AIModelConfig, type ResolvedAIProviderConfig } from "../ai/providerOptions.js";
|
|
3
|
+
import { type ResolvedMaxTokenUsageOptions } from "../ai/tokenUsage.js";
|
|
4
|
+
import { type TranslationPageContext } from "../payload/documentTranslation.js";
|
|
5
|
+
type TranslateDocumentOptions = {
|
|
6
|
+
allowUserApiKeys?: boolean;
|
|
7
|
+
maxOutputTokens?: number;
|
|
8
|
+
maxTokenUsage?: ResolvedMaxTokenUsageOptions;
|
|
9
|
+
models?: AIModelConfig;
|
|
10
|
+
pageContexts: Map<string, TranslationPageContext>;
|
|
11
|
+
providers?: ResolvedAIProviderConfig[];
|
|
12
|
+
};
|
|
13
|
+
export declare const createTranslateDocumentHandler: (options: TranslateDocumentOptions) => PayloadHandler;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { generateText } from "ai";
|
|
2
|
+
import { isAIProvider } from "../ai/providerOptions.js";
|
|
3
|
+
import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
|
|
4
|
+
import { getExceededTokenUsageLimit, recordTokenUsage } from "../ai/tokenUsage.js";
|
|
5
|
+
import { getTranslationValues } from "../payload/documentTranslation.js";
|
|
6
|
+
const getDefaultLocale = (req)=>{
|
|
7
|
+
const localization = req.payload.config.localization;
|
|
8
|
+
if (!localization) return null;
|
|
9
|
+
return localization.defaultLocale || null;
|
|
10
|
+
};
|
|
11
|
+
const loadDocument = async ({ context, id, locale, req })=>{
|
|
12
|
+
if (context.type === "collection") {
|
|
13
|
+
if (id === undefined || id === null || id === "") return null;
|
|
14
|
+
return req.payload.findByID({
|
|
15
|
+
collection: context.slug,
|
|
16
|
+
depth: 0,
|
|
17
|
+
fallbackLocale: false,
|
|
18
|
+
id,
|
|
19
|
+
locale: locale,
|
|
20
|
+
overrideAccess: false,
|
|
21
|
+
req
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return req.payload.findGlobal({
|
|
25
|
+
depth: 0,
|
|
26
|
+
fallbackLocale: false,
|
|
27
|
+
locale: locale,
|
|
28
|
+
overrideAccess: false,
|
|
29
|
+
req,
|
|
30
|
+
slug: context.slug
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const parseTranslations = (text, sourceCount)=>{
|
|
34
|
+
const value = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
35
|
+
const parsed = JSON.parse(value);
|
|
36
|
+
if (!parsed.translations || typeof parsed.translations !== "object") throw new Error("Invalid translation response.");
|
|
37
|
+
return Array.from({
|
|
38
|
+
length: sourceCount
|
|
39
|
+
}, (_, index)=>parsed.translations?.[String(index)]);
|
|
40
|
+
};
|
|
41
|
+
export const createTranslateDocumentHandler = (options)=>async (req)=>{
|
|
42
|
+
if (!req.user) return Response.json({
|
|
43
|
+
error: "Unauthorized"
|
|
44
|
+
}, {
|
|
45
|
+
status: 401
|
|
46
|
+
});
|
|
47
|
+
const body = req.json ? await req.json().catch(()=>null) : null;
|
|
48
|
+
const locale = body?.locale?.trim();
|
|
49
|
+
const scopeSlug = body?.scope?.slug?.trim();
|
|
50
|
+
const scopeType = body?.scope?.type;
|
|
51
|
+
if (!body?.action || !locale || !scopeSlug || !scopeType) {
|
|
52
|
+
return Response.json({
|
|
53
|
+
error: "Action, locale, and page scope are required."
|
|
54
|
+
}, {
|
|
55
|
+
status: 400
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const pageContext = options.pageContexts.get(`${scopeType}:${scopeSlug}`);
|
|
59
|
+
const defaultLocale = getDefaultLocale(req);
|
|
60
|
+
if (!pageContext || !defaultLocale || locale === defaultLocale) {
|
|
61
|
+
return Response.json({
|
|
62
|
+
available: false
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
// Payload derives locale state on the request, so reads sharing one req must not run concurrently.
|
|
67
|
+
const sourceDocument = await loadDocument({
|
|
68
|
+
context: pageContext,
|
|
69
|
+
id: body.id,
|
|
70
|
+
locale: defaultLocale,
|
|
71
|
+
req
|
|
72
|
+
});
|
|
73
|
+
const targetDocument = await loadDocument({
|
|
74
|
+
context: pageContext,
|
|
75
|
+
id: body.id,
|
|
76
|
+
locale,
|
|
77
|
+
req
|
|
78
|
+
});
|
|
79
|
+
const sourceValues = getTranslationValues(pageContext.fields, sourceDocument);
|
|
80
|
+
const targetValues = getTranslationValues(pageContext.fields, targetDocument);
|
|
81
|
+
const populatedTargetPaths = new Set(targetValues.map((entry)=>entry.path));
|
|
82
|
+
const missingSourceValues = sourceValues.filter((entry)=>!populatedTargetPaths.has(entry.path));
|
|
83
|
+
const available = missingSourceValues.length > 0;
|
|
84
|
+
if (body.action === "status" || !available) return Response.json({
|
|
85
|
+
available
|
|
86
|
+
});
|
|
87
|
+
const user = req.user;
|
|
88
|
+
const exceededLimit = await getExceededTokenUsageLimit({
|
|
89
|
+
maxTokenUsage: options.maxTokenUsage,
|
|
90
|
+
req,
|
|
91
|
+
userID: user.id
|
|
92
|
+
});
|
|
93
|
+
if (exceededLimit) return Response.json({
|
|
94
|
+
error: "AI token usage limit reached."
|
|
95
|
+
}, {
|
|
96
|
+
status: 429
|
|
97
|
+
});
|
|
98
|
+
const managedProviders = options.providers?.length ? options.providers : null;
|
|
99
|
+
const requestedProvider = body.provider || (managedProviders ? managedProviders[0].id : user.aiProvider || "openai");
|
|
100
|
+
const managedProvider = managedProviders?.find((provider)=>provider.id === requestedProvider);
|
|
101
|
+
if (managedProviders && !managedProvider) {
|
|
102
|
+
return Response.json({
|
|
103
|
+
error: `Unsupported AI provider: ${requestedProvider}`
|
|
104
|
+
}, {
|
|
105
|
+
status: 400
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (!managedProvider && !isAIProvider(requestedProvider)) {
|
|
109
|
+
return Response.json({
|
|
110
|
+
error: `Unsupported AI provider: ${requestedProvider}`
|
|
111
|
+
}, {
|
|
112
|
+
status: 400
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const provider = managedProvider?.provider || requestedProvider;
|
|
116
|
+
const requestedModel = body.model || managedProvider?.defaultModel;
|
|
117
|
+
const providerConfig = getProviderConfig({
|
|
118
|
+
apiKey: managedProvider ? managedProvider.apiKey : options.allowUserApiKeys === false ? null : user.aiApiKey,
|
|
119
|
+
defaultModels: options.models?.defaults,
|
|
120
|
+
model: requestedModel,
|
|
121
|
+
provider
|
|
122
|
+
});
|
|
123
|
+
if (!providerConfig.apiKey) return Response.json({
|
|
124
|
+
error: "Configure an AI provider API key first."
|
|
125
|
+
}, {
|
|
126
|
+
status: 400
|
|
127
|
+
});
|
|
128
|
+
const model = await getModel({
|
|
129
|
+
apiKey: providerConfig.apiKey,
|
|
130
|
+
...managedProvider?.baseURL ? {
|
|
131
|
+
baseURL: managedProvider.baseURL
|
|
132
|
+
} : {},
|
|
133
|
+
model: providerConfig.modelID,
|
|
134
|
+
provider
|
|
135
|
+
});
|
|
136
|
+
const result = await generateText({
|
|
137
|
+
maxOutputTokens: options.maxOutputTokens || 700,
|
|
138
|
+
model,
|
|
139
|
+
prompt: JSON.stringify({
|
|
140
|
+
sourceLocale: defaultLocale,
|
|
141
|
+
targetLocale: locale,
|
|
142
|
+
values: missingSourceValues.map((entry, index)=>({
|
|
143
|
+
id: String(index),
|
|
144
|
+
value: entry.value
|
|
145
|
+
}))
|
|
146
|
+
}),
|
|
147
|
+
system: [
|
|
148
|
+
"Translate the human-readable content in every value to the target locale.",
|
|
149
|
+
"Preserve JSON structure, keys, IDs, URLs, numbers, booleans, nulls, and Lexical rich-text structure exactly.",
|
|
150
|
+
'Return strict JSON in the shape {"translations":{"0":translatedValue}} with one entry for every input ID.',
|
|
151
|
+
"Return no Markdown or explanation."
|
|
152
|
+
].join(" ")
|
|
153
|
+
});
|
|
154
|
+
const translatedValues = parseTranslations(result.text, missingSourceValues.length);
|
|
155
|
+
if (translatedValues.some((value)=>value === undefined)) throw new Error("Translation response is incomplete.");
|
|
156
|
+
if (result.usage && options.maxTokenUsage) {
|
|
157
|
+
await recordTokenUsage({
|
|
158
|
+
model: providerConfig.modelID,
|
|
159
|
+
provider: managedProvider?.id || provider,
|
|
160
|
+
req,
|
|
161
|
+
usage: result.usage,
|
|
162
|
+
userID: user.id
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return Response.json({
|
|
166
|
+
available: true,
|
|
167
|
+
values: missingSourceValues.map((entry, index)=>({
|
|
168
|
+
fieldType: entry.fieldType,
|
|
169
|
+
path: entry.path,
|
|
170
|
+
value: translatedValues[index]
|
|
171
|
+
}))
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
req.payload.logger.error({
|
|
175
|
+
err: error,
|
|
176
|
+
msg: "AI document translation failed",
|
|
177
|
+
scopeSlug,
|
|
178
|
+
scopeType
|
|
179
|
+
});
|
|
180
|
+
return Response.json({
|
|
181
|
+
error: "AI document translation failed."
|
|
182
|
+
}, {
|
|
183
|
+
status: 500
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,14 @@ import { type MaxTokenUsageOptions } from "./ai/tokenUsage.js";
|
|
|
4
4
|
import { type CollectionPermissionMap } from "./payload/collectionPermissions.js";
|
|
5
5
|
export type { AIModelConfig, AIProviderConfig, AIProviderModelOption } from "./ai/providerOptions.js";
|
|
6
6
|
export type { MaxTokenUsageOptions } from "./ai/tokenUsage.js";
|
|
7
|
+
export type CollectionTypeAIOptions = {
|
|
8
|
+
aiInput?: boolean;
|
|
9
|
+
generateFields?: boolean;
|
|
10
|
+
};
|
|
7
11
|
export type PayloadAIPluginOptions = {
|
|
8
12
|
aiInput?: boolean;
|
|
9
13
|
allowUserApiKeys?: boolean;
|
|
14
|
+
authCollections?: CollectionTypeAIOptions;
|
|
10
15
|
collections?: CollectionPermissionMap;
|
|
11
16
|
disabled?: boolean;
|
|
12
17
|
generateFields?: boolean;
|
|
@@ -19,6 +24,9 @@ export type PayloadAIPluginOptions = {
|
|
|
19
24
|
};
|
|
20
25
|
models?: AIModelConfig;
|
|
21
26
|
maxTokenUsage?: MaxTokenUsageOptions;
|
|
27
|
+
promptCaching?: boolean;
|
|
22
28
|
providers?: AIProviderConfig[];
|
|
29
|
+
translate?: boolean;
|
|
30
|
+
uploadCollections?: CollectionTypeAIOptions;
|
|
23
31
|
};
|
|
24
32
|
export declare const payloadAiPlugin: (pluginOptions: PayloadAIPluginOptions) => (config: Config) => Config;
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveMaxTokenUsageOptions, tokenUsageCollectionSlug } from "./ai/toke
|
|
|
3
3
|
import { createApplyActionHandler } from "./handlers/applyActionHandler.js";
|
|
4
4
|
import { createChatHandler } from "./handlers/chatHandler.js";
|
|
5
5
|
import { createGenerateFieldHandler } from "./handlers/generateFieldHandler.js";
|
|
6
|
+
import { createTranslateDocumentHandler } from "./handlers/translateDocumentHandler.js";
|
|
6
7
|
import { createMentionSuggestionHandler } from "./handlers/mentionSuggestionHandler.js";
|
|
7
8
|
import { createMediaUploadHandler } from "./handlers/mediaUploadHandler.js";
|
|
8
9
|
import { createProposalDiffHandler } from "./handlers/proposalDiffHandler.js";
|
|
@@ -10,6 +11,7 @@ import { createAuditLogHandler } from "./handlers/auditLogHandler.js";
|
|
|
10
11
|
import { resolveCollectionPermissions } from "./payload/collectionPermissions.js";
|
|
11
12
|
import { isInternalCollection } from "./payload/shared.js";
|
|
12
13
|
import { addTextGenerationFields } from "./payload/textFieldGeneration.js";
|
|
14
|
+
import { hasLocalizedFields } from "./payload/documentTranslation.js";
|
|
13
15
|
const resolveMediaUploadOptions = (media)=>{
|
|
14
16
|
if (!media || media.enabled === false) return null;
|
|
15
17
|
return {
|
|
@@ -190,6 +192,7 @@ const createAITokenUsageCollection = ()=>({
|
|
|
190
192
|
],
|
|
191
193
|
timestamps: true
|
|
192
194
|
});
|
|
195
|
+
const hasAuthenticatedUser = (_data, _siblingData, { user })=>Boolean(user);
|
|
193
196
|
const addAccountFields = ({ allowUserApiKeys, config })=>{
|
|
194
197
|
const adminUserSlug = config.admin?.user;
|
|
195
198
|
if (!adminUserSlug || !config.collections) return;
|
|
@@ -198,6 +201,9 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
|
|
|
198
201
|
userCollection.fields.push({
|
|
199
202
|
name: "aiProvider",
|
|
200
203
|
type: "select",
|
|
204
|
+
admin: {
|
|
205
|
+
condition: hasAuthenticatedUser
|
|
206
|
+
},
|
|
201
207
|
defaultValue: "openai",
|
|
202
208
|
label: "AI Provider",
|
|
203
209
|
options: aiProviders
|
|
@@ -207,6 +213,7 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
|
|
|
207
213
|
name: "aiApiKey",
|
|
208
214
|
type: "text",
|
|
209
215
|
admin: {
|
|
216
|
+
condition: hasAuthenticatedUser,
|
|
210
217
|
components: {
|
|
211
218
|
Field: "@mvriu5/payload-ai/client#AIApiKeyField"
|
|
212
219
|
},
|
|
@@ -220,11 +227,13 @@ const aiField = {
|
|
|
220
227
|
name: "payloadAi",
|
|
221
228
|
type: "ui",
|
|
222
229
|
admin: {
|
|
230
|
+
condition: hasAuthenticatedUser,
|
|
223
231
|
components: {
|
|
224
232
|
Field: "@mvriu5/payload-ai/client#AIInput"
|
|
225
233
|
}
|
|
226
234
|
}
|
|
227
235
|
};
|
|
236
|
+
const translateDocumentComponent = "@mvriu5/payload-ai/client#TranslateDocumentButton";
|
|
228
237
|
const getEntityLabel = (label, fallback)=>{
|
|
229
238
|
if (typeof label === "string") return label;
|
|
230
239
|
if (label && typeof label === "object") {
|
|
@@ -233,12 +242,15 @@ const getEntityLabel = (label, fallback)=>{
|
|
|
233
242
|
}
|
|
234
243
|
return fallback;
|
|
235
244
|
};
|
|
236
|
-
const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, config })=>{
|
|
245
|
+
const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, addTranslation, authCollections, config, uploadCollections })=>{
|
|
237
246
|
const pageContexts = new Map();
|
|
247
|
+
const translationPageContexts = new Map();
|
|
238
248
|
for (const collection of config.collections || []){
|
|
239
249
|
if (isInternalCollection(collection.slug)) continue;
|
|
240
250
|
if (collection.slug === "payload-ai-auditlog") continue;
|
|
241
|
-
|
|
251
|
+
const allowAIInput = addAIInput && (!collection.auth || authCollections?.aiInput === true) && (!collection.upload || uploadCollections?.aiInput === true);
|
|
252
|
+
const allowGenerateFields = addGenerateFields && (!collection.auth || authCollections?.generateFields === true) && (!collection.upload || uploadCollections?.generateFields === true);
|
|
253
|
+
if (allowGenerateFields) {
|
|
242
254
|
const pageContext = addTextGenerationFields({
|
|
243
255
|
fields: collection.fields || [],
|
|
244
256
|
label: getEntityLabel(collection.labels?.singular, collection.slug),
|
|
@@ -247,10 +259,28 @@ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, confi
|
|
|
247
259
|
});
|
|
248
260
|
pageContexts.set(`collection:${collection.slug}`, pageContext);
|
|
249
261
|
}
|
|
250
|
-
if (
|
|
262
|
+
if (allowAIInput) collection.fields = [
|
|
251
263
|
aiField,
|
|
252
264
|
...collection.fields || []
|
|
253
265
|
];
|
|
266
|
+
if (addTranslation && hasLocalizedFields(collection.fields || [])) {
|
|
267
|
+
translationPageContexts.set(`collection:${collection.slug}`, {
|
|
268
|
+
fields: collection.fields || [],
|
|
269
|
+
label: getEntityLabel(collection.labels?.singular, collection.slug),
|
|
270
|
+
slug: collection.slug,
|
|
271
|
+
type: "collection"
|
|
272
|
+
});
|
|
273
|
+
collection.admin = collection.admin || {};
|
|
274
|
+
collection.admin.components = collection.admin.components || {};
|
|
275
|
+
collection.admin.components.edit = collection.admin.components.edit || {};
|
|
276
|
+
const controls = collection.admin.components.edit.beforeDocumentControls || [];
|
|
277
|
+
if (!controls.includes(translateDocumentComponent)) {
|
|
278
|
+
collection.admin.components.edit.beforeDocumentControls = [
|
|
279
|
+
...controls,
|
|
280
|
+
translateDocumentComponent
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
254
284
|
}
|
|
255
285
|
for (const global of config.globals || []){
|
|
256
286
|
if (addGenerateFields) {
|
|
@@ -266,8 +296,29 @@ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, confi
|
|
|
266
296
|
aiField,
|
|
267
297
|
...global.fields || []
|
|
268
298
|
];
|
|
299
|
+
if (addTranslation && hasLocalizedFields(global.fields || [])) {
|
|
300
|
+
translationPageContexts.set(`global:${global.slug}`, {
|
|
301
|
+
fields: global.fields || [],
|
|
302
|
+
label: getEntityLabel(global.label, global.slug),
|
|
303
|
+
slug: global.slug,
|
|
304
|
+
type: "global"
|
|
305
|
+
});
|
|
306
|
+
global.admin = global.admin || {};
|
|
307
|
+
global.admin.components = global.admin.components || {};
|
|
308
|
+
global.admin.components.elements = global.admin.components.elements || {};
|
|
309
|
+
const controls = global.admin.components.elements.beforeDocumentControls || [];
|
|
310
|
+
if (!controls.includes(translateDocumentComponent)) {
|
|
311
|
+
global.admin.components.elements.beforeDocumentControls = [
|
|
312
|
+
...controls,
|
|
313
|
+
translateDocumentComponent
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
269
317
|
}
|
|
270
|
-
return
|
|
318
|
+
return {
|
|
319
|
+
textGenerationPageContexts: pageContexts,
|
|
320
|
+
translationPageContexts
|
|
321
|
+
};
|
|
271
322
|
};
|
|
272
323
|
export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
273
324
|
const incomingOnInit = config.onInit;
|
|
@@ -292,10 +343,13 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
|
292
343
|
});
|
|
293
344
|
if (pluginOptions.disabled) return config;
|
|
294
345
|
const generateFields = pluginOptions.generateFields !== false;
|
|
295
|
-
const textGenerationPageContexts = addAIFieldsToDocumentsAndGlobals({
|
|
346
|
+
const { textGenerationPageContexts, translationPageContexts } = addAIFieldsToDocumentsAndGlobals({
|
|
296
347
|
addGenerateFields: generateFields,
|
|
297
348
|
addAIInput: pluginOptions.aiInput !== false,
|
|
298
|
-
|
|
349
|
+
addTranslation: pluginOptions.translate !== false,
|
|
350
|
+
authCollections: pluginOptions.authCollections,
|
|
351
|
+
config,
|
|
352
|
+
uploadCollections: pluginOptions.uploadCollections
|
|
299
353
|
});
|
|
300
354
|
const mentionCollectionSlugs = config.collections.flatMap((collection)=>{
|
|
301
355
|
if (isInternalCollection(collection.slug)) return [];
|
|
@@ -333,6 +387,7 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
|
333
387
|
maxOutputTokens,
|
|
334
388
|
maxTokenUsage,
|
|
335
389
|
models: modelConfig,
|
|
390
|
+
promptCaching: pluginOptions.promptCaching !== false,
|
|
336
391
|
providers: providerConfigs
|
|
337
392
|
}),
|
|
338
393
|
method: "post",
|
|
@@ -381,6 +436,20 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
|
381
436
|
path: "/ai-generate-field"
|
|
382
437
|
});
|
|
383
438
|
}
|
|
439
|
+
if (translationPageContexts.size > 0) {
|
|
440
|
+
config.endpoints.push({
|
|
441
|
+
handler: createTranslateDocumentHandler({
|
|
442
|
+
allowUserApiKeys,
|
|
443
|
+
maxOutputTokens,
|
|
444
|
+
maxTokenUsage,
|
|
445
|
+
models: modelConfig,
|
|
446
|
+
pageContexts: translationPageContexts,
|
|
447
|
+
providers: providerConfigs
|
|
448
|
+
}),
|
|
449
|
+
method: "post",
|
|
450
|
+
path: "/ai-translate-document"
|
|
451
|
+
});
|
|
452
|
+
}
|
|
384
453
|
if (mediaUploadOptions) {
|
|
385
454
|
config.endpoints.push({
|
|
386
455
|
handler: createMediaUploadHandler(mediaUploadOptions),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
export type TranslationFieldType = "json" | "richText" | "text" | "textarea" | "unknown";
|
|
3
|
+
export type TranslationPageContext = {
|
|
4
|
+
fields: Field[];
|
|
5
|
+
label: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
type: "collection" | "global";
|
|
8
|
+
};
|
|
9
|
+
export type TranslationValue = {
|
|
10
|
+
fieldType: TranslationFieldType;
|
|
11
|
+
path: string;
|
|
12
|
+
value: unknown;
|
|
13
|
+
};
|
|
14
|
+
export declare const getTranslationValues: (fields: Field[], data: unknown) => TranslationValue[];
|
|
15
|
+
export declare const hasLocalizedFields: (fields: Field[]) => boolean;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const hasValue = (value)=>{
|
|
2
|
+
if (value === null || value === undefined || value === "") return false;
|
|
3
|
+
if (Array.isArray(value)) return value.some(hasValue);
|
|
4
|
+
if (typeof value === "object") return Object.values(value).some(hasValue);
|
|
5
|
+
return true;
|
|
6
|
+
};
|
|
7
|
+
const hasRichTextValue = (value)=>{
|
|
8
|
+
if (!value || typeof value !== "object") return false;
|
|
9
|
+
if (Array.isArray(value)) return value.some(hasRichTextValue);
|
|
10
|
+
const node = value;
|
|
11
|
+
if (node.type === "text") return typeof node.text === "string" && node.text.trim().length > 0;
|
|
12
|
+
if ([
|
|
13
|
+
"block",
|
|
14
|
+
"inlineBlock",
|
|
15
|
+
"relationship",
|
|
16
|
+
"upload"
|
|
17
|
+
].includes(String(node.type))) return true;
|
|
18
|
+
if (node.root) return hasRichTextValue(node.root);
|
|
19
|
+
return Array.isArray(node.children) && node.children.some(hasRichTextValue);
|
|
20
|
+
};
|
|
21
|
+
const getFieldType = (field)=>[
|
|
22
|
+
"json",
|
|
23
|
+
"richText",
|
|
24
|
+
"text",
|
|
25
|
+
"textarea"
|
|
26
|
+
].includes(field.type) ? field.type : "unknown";
|
|
27
|
+
const collectFields = ({ data, fields, parentPath = "", result })=>{
|
|
28
|
+
for (const field of fields){
|
|
29
|
+
if (field.type === "tabs") {
|
|
30
|
+
for (const tab of field.tabs){
|
|
31
|
+
const tabData = "name" in tab && tab.name ? data[tab.name] || {} : data;
|
|
32
|
+
collectFields({
|
|
33
|
+
data: tabData,
|
|
34
|
+
fields: tab.fields,
|
|
35
|
+
parentPath: "name" in tab && tab.name ? parentPath ? `${parentPath}.${tab.name}` : tab.name : parentPath,
|
|
36
|
+
result
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!("name" in field) || !field.name) {
|
|
42
|
+
if ("fields" in field && Array.isArray(field.fields)) collectFields({
|
|
43
|
+
data,
|
|
44
|
+
fields: field.fields,
|
|
45
|
+
parentPath,
|
|
46
|
+
result
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const path = parentPath ? `${parentPath}.${field.name}` : field.name;
|
|
51
|
+
const value = data[field.name];
|
|
52
|
+
if ("localized" in field && field.localized) {
|
|
53
|
+
const valueExists = field.type === "richText" ? hasRichTextValue(value) : hasValue(value);
|
|
54
|
+
if (valueExists) {
|
|
55
|
+
result.push({
|
|
56
|
+
fieldType: getFieldType(field),
|
|
57
|
+
path,
|
|
58
|
+
value
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (field.type === "array" && Array.isArray(value)) {
|
|
64
|
+
value.forEach((row, index)=>{
|
|
65
|
+
if (row && typeof row === "object") {
|
|
66
|
+
collectFields({
|
|
67
|
+
data: row,
|
|
68
|
+
fields: field.fields,
|
|
69
|
+
parentPath: `${path}.${index}`,
|
|
70
|
+
result
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (field.type === "blocks" && Array.isArray(value)) {
|
|
77
|
+
value.forEach((row, index)=>{
|
|
78
|
+
if (!row || typeof row !== "object") return;
|
|
79
|
+
const rowData = row;
|
|
80
|
+
const block = field.blocks.find((candidate)=>typeof candidate === "object" && candidate.slug === rowData.blockType);
|
|
81
|
+
if (block && typeof block === "object") {
|
|
82
|
+
collectFields({
|
|
83
|
+
data: rowData,
|
|
84
|
+
fields: block.fields,
|
|
85
|
+
parentPath: `${path}.${index}`,
|
|
86
|
+
result
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if ("fields" in field && Array.isArray(field.fields) && value && typeof value === "object") {
|
|
93
|
+
collectFields({
|
|
94
|
+
data: value,
|
|
95
|
+
fields: field.fields,
|
|
96
|
+
parentPath: path,
|
|
97
|
+
result
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export const getTranslationValues = (fields, data)=>{
|
|
103
|
+
if (!data || typeof data !== "object") return [];
|
|
104
|
+
const result = [];
|
|
105
|
+
collectFields({
|
|
106
|
+
data: data,
|
|
107
|
+
fields,
|
|
108
|
+
result
|
|
109
|
+
});
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
export const hasLocalizedFields = (fields)=>{
|
|
113
|
+
for (const field of fields){
|
|
114
|
+
if ("localized" in field && field.localized) return true;
|
|
115
|
+
if (field.type === "tabs" && field.tabs.some((tab)=>hasLocalizedFields(tab.fields))) return true;
|
|
116
|
+
if (field.type === "blocks" && field.blocks.some((block)=>typeof block === "object" && hasLocalizedFields(block.fields))) return true;
|
|
117
|
+
if ("fields" in field && Array.isArray(field.fields) && hasLocalizedFields(field.fields)) return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type CollectionConfig } from "./normalizeData.js";
|
|
3
|
+
export declare const maxToolSelectedFields = 12;
|
|
4
|
+
export type ReadCollectionConfig = CollectionConfig & {
|
|
5
|
+
admin?: {
|
|
6
|
+
useAsTitle?: string;
|
|
7
|
+
};
|
|
8
|
+
timestamps?: boolean;
|
|
9
|
+
upload?: boolean | Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
export declare const getToolSelectableFieldNames: (config: ReadCollectionConfig) => string[];
|
|
12
|
+
export declare const createToolFieldNamesSchema: (configs: ReadCollectionConfig[]) => z.ZodOptional<z.ZodArray<z.ZodString | z.ZodEnum<{
|
|
13
|
+
[x: string]: string;
|
|
14
|
+
}>>>;
|
|
15
|
+
export declare const resolveToolFieldSelection: ({ config, mode, requestedFields, }: {
|
|
16
|
+
config: ReadCollectionConfig;
|
|
17
|
+
mode?: "document" | "search";
|
|
18
|
+
requestedFields?: string[];
|
|
19
|
+
}) => {
|
|
20
|
+
allowedFields: string[];
|
|
21
|
+
depth: number;
|
|
22
|
+
fields: string[];
|
|
23
|
+
invalidFields: string[];
|
|
24
|
+
select: Record<string, true>;
|
|
25
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isSensitiveKey } from "../ai/sensitiveData.js";
|
|
3
|
+
import { getSchemaFields } from "./normalizeData.js";
|
|
4
|
+
export const maxToolSelectedFields = 12;
|
|
5
|
+
const cheapFieldTypes = new Set([
|
|
6
|
+
"checkbox",
|
|
7
|
+
"date",
|
|
8
|
+
"email",
|
|
9
|
+
"number",
|
|
10
|
+
"point",
|
|
11
|
+
"radio",
|
|
12
|
+
"select",
|
|
13
|
+
"text"
|
|
14
|
+
]);
|
|
15
|
+
const populatedFieldTypes = new Set([
|
|
16
|
+
"relationship",
|
|
17
|
+
"upload"
|
|
18
|
+
]);
|
|
19
|
+
const systemFieldNames = [
|
|
20
|
+
"id",
|
|
21
|
+
"createdAt",
|
|
22
|
+
"updatedAt"
|
|
23
|
+
];
|
|
24
|
+
const uploadFieldNames = [
|
|
25
|
+
"filename",
|
|
26
|
+
"mimeType",
|
|
27
|
+
"filesize",
|
|
28
|
+
"width",
|
|
29
|
+
"height",
|
|
30
|
+
"url",
|
|
31
|
+
"thumbnailURL"
|
|
32
|
+
];
|
|
33
|
+
const getSelectableFields = (config)=>{
|
|
34
|
+
return getSchemaFields(config).filter((field)=>{
|
|
35
|
+
return Boolean(field.name && field.type !== "ui" && !isSensitiveKey(field.name));
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
export const getToolSelectableFieldNames = (config)=>{
|
|
39
|
+
const names = getSelectableFields(config).flatMap((field)=>field.name ? [
|
|
40
|
+
field.name
|
|
41
|
+
] : []);
|
|
42
|
+
names.push("id");
|
|
43
|
+
if (config.timestamps !== false) names.push("createdAt", "updatedAt");
|
|
44
|
+
if (config.upload) names.push(...uploadFieldNames);
|
|
45
|
+
return [
|
|
46
|
+
...new Set(names)
|
|
47
|
+
];
|
|
48
|
+
};
|
|
49
|
+
const getDefaultFieldNames = (config, mode)=>{
|
|
50
|
+
const selectableFields = getSelectableFields(config);
|
|
51
|
+
const selectableNames = new Set(getToolSelectableFieldNames(config));
|
|
52
|
+
const candidates = [
|
|
53
|
+
config.admin?.useAsTitle,
|
|
54
|
+
"title",
|
|
55
|
+
"name",
|
|
56
|
+
"label",
|
|
57
|
+
"headline",
|
|
58
|
+
"slug",
|
|
59
|
+
"_status",
|
|
60
|
+
...config.upload ? selectableFields.flatMap((field)=>field.name && cheapFieldTypes.has(field.type || "") ? [
|
|
61
|
+
field.name
|
|
62
|
+
] : []).slice(0, 2) : [],
|
|
63
|
+
...mode === "document" ? [
|
|
64
|
+
"updatedAt"
|
|
65
|
+
] : [],
|
|
66
|
+
...config.upload ? [
|
|
67
|
+
"filename",
|
|
68
|
+
"mimeType",
|
|
69
|
+
"filesize",
|
|
70
|
+
"url"
|
|
71
|
+
] : []
|
|
72
|
+
];
|
|
73
|
+
const defaults = candidates.filter((name)=>Boolean(name && (selectableNames.has(name) || systemFieldNames.includes(name))));
|
|
74
|
+
if (!defaults.some((fieldName)=>!systemFieldNames.includes(fieldName) && !uploadFieldNames.includes(fieldName))) {
|
|
75
|
+
const firstCheapField = selectableFields.find((field)=>field.name && cheapFieldTypes.has(field.type || ""));
|
|
76
|
+
const firstSelectableField = firstCheapField || selectableFields[0];
|
|
77
|
+
defaults.unshift(firstSelectableField?.name || "id");
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
...new Set(defaults)
|
|
81
|
+
].slice(0, mode === "search" ? 4 : 6);
|
|
82
|
+
};
|
|
83
|
+
export const createToolFieldNamesSchema = (configs)=>{
|
|
84
|
+
const names = [
|
|
85
|
+
...new Set(configs.flatMap(getToolSelectableFieldNames))
|
|
86
|
+
];
|
|
87
|
+
const fieldNameSchema = names.length > 0 ? z.enum(names) : z.string().refine(()=>false, "No fields are selectable.");
|
|
88
|
+
return z.array(fieldNameSchema).max(maxToolSelectedFields).optional();
|
|
89
|
+
};
|
|
90
|
+
export const resolveToolFieldSelection = ({ config, mode = "document", requestedFields })=>{
|
|
91
|
+
const allowedNames = new Set(getToolSelectableFieldNames(config));
|
|
92
|
+
const normalizedRequestedFields = [
|
|
93
|
+
...new Set((requestedFields || []).slice(0, maxToolSelectedFields).map((field)=>field.trim()).filter(Boolean))
|
|
94
|
+
];
|
|
95
|
+
const invalidFields = normalizedRequestedFields.filter((field)=>!allowedNames.has(field));
|
|
96
|
+
const selectedFields = [
|
|
97
|
+
...new Set((requestedFields?.length ? requestedFields : getDefaultFieldNames(config, mode)).map((field)=>field.trim()).filter((field)=>allowedNames.has(field)))
|
|
98
|
+
].slice(0, maxToolSelectedFields);
|
|
99
|
+
const fields = selectedFields.length > 0 ? selectedFields : getDefaultFieldNames(config, mode);
|
|
100
|
+
const fieldTypesByName = new Map(getSelectableFields(config).flatMap((field)=>field.name ? [
|
|
101
|
+
[
|
|
102
|
+
field.name,
|
|
103
|
+
field.type
|
|
104
|
+
]
|
|
105
|
+
] : []));
|
|
106
|
+
return {
|
|
107
|
+
allowedFields: [
|
|
108
|
+
...allowedNames
|
|
109
|
+
],
|
|
110
|
+
depth: fields.some((field)=>populatedFieldTypes.has(fieldTypesByName.get(field) || "")) ? 1 : 0,
|
|
111
|
+
fields,
|
|
112
|
+
invalidFields,
|
|
113
|
+
select: Object.fromEntries(fields.map((field)=>[
|
|
114
|
+
field,
|
|
115
|
+
true
|
|
116
|
+
]))
|
|
117
|
+
};
|
|
118
|
+
};
|