@mvriu5/payload-ai 1.3.2 → 1.5.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 (41) hide show
  1. package/README.md +102 -13
  2. package/dist/ai/providerOptions.d.ts +24 -1
  3. package/dist/ai/providerOptions.js +81 -0
  4. package/dist/ai/providerRuntime.d.ts +2 -1
  5. package/dist/ai/providerRuntime.js +5 -2
  6. package/dist/ai/tokenUsage.d.ts +38 -0
  7. package/dist/ai/tokenUsage.js +106 -0
  8. package/dist/components/Icons.d.ts +1 -0
  9. package/dist/components/Icons.js +15 -0
  10. package/dist/components/action-toast/ActionToast.d.ts +5 -1
  11. package/dist/components/action-toast/ActionToast.js +57 -21
  12. package/dist/components/ai-input/AIInput.d.ts +4 -1
  13. package/dist/components/ai-input/AIInput.js +196 -51
  14. package/dist/components/ai-input/AIInput.module.css +89 -2
  15. package/dist/components/audit-log-list/AuditLogList.js +9 -2
  16. package/dist/components/dashboard/Dashboard.js +3 -1
  17. package/dist/components/generate-field/GenerateField.d.ts +11 -0
  18. package/dist/components/generate-field/GenerateField.js +109 -0
  19. package/dist/components/generate-field/GenerateField.module.css +39 -0
  20. package/dist/components/generate-field/richText.d.ts +29 -0
  21. package/dist/components/generate-field/richText.js +30 -0
  22. package/dist/components/hooks/useAIChatStream.d.ts +12 -1
  23. package/dist/components/hooks/useAIChatStream.js +13 -4
  24. package/dist/components/hooks/useAISettings.d.ts +6 -4
  25. package/dist/components/hooks/useAISettings.js +62 -22
  26. package/dist/components/hooks/usePluginConfig.d.ts +8 -20
  27. package/dist/components/hooks/usePluginConfig.js +9 -2
  28. package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
  29. package/dist/components/text-shimmer/TextShimmer.js +34 -0
  30. package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
  31. package/dist/exports/client.d.ts +4 -0
  32. package/dist/exports/client.js +4 -0
  33. package/dist/handlers/chatHandler.d.ts +4 -1
  34. package/dist/handlers/chatHandler.js +198 -17
  35. package/dist/handlers/generateFieldHandler.d.ts +14 -0
  36. package/dist/handlers/generateFieldHandler.js +144 -0
  37. package/dist/index.d.ts +8 -1
  38. package/dist/index.js +141 -5
  39. package/dist/payload/textFieldGeneration.d.ts +26 -0
  40. package/dist/payload/textFieldGeneration.js +99 -0
  41. package/package.json +18 -16
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
- import { aiProviders, getResolvedAIModelConfig } from "./ai/providerOptions.js";
1
+ import { aiProviders, getResolvedAIModelConfig, resolveAIProviderConfigs, toClientAIProviderProfiles } from "./ai/providerOptions.js";
2
+ import { resolveMaxTokenUsageOptions, tokenUsageCollectionSlug } from "./ai/tokenUsage.js";
2
3
  import { createApplyActionHandler } from "./handlers/applyActionHandler.js";
3
4
  import { createChatHandler } from "./handlers/chatHandler.js";
5
+ import { createGenerateFieldHandler } from "./handlers/generateFieldHandler.js";
4
6
  import { createMentionSuggestionHandler } from "./handlers/mentionSuggestionHandler.js";
5
7
  import { createMediaUploadHandler } from "./handlers/mediaUploadHandler.js";
6
8
  import { createProposalDiffHandler } from "./handlers/proposalDiffHandler.js";
7
9
  import { createAuditLogHandler } from "./handlers/auditLogHandler.js";
8
10
  import { resolveCollectionPermissions } from "./payload/collectionPermissions.js";
9
11
  import { isInternalCollection } from "./payload/shared.js";
12
+ import { addTextGenerationFields } from "./payload/textFieldGeneration.js";
10
13
  const resolveMediaUploadOptions = (media)=>{
11
14
  if (!media || media.enabled === false) return null;
12
15
  return {
@@ -137,6 +140,56 @@ const createAIChangesCollection = ()=>({
137
140
  ],
138
141
  timestamps: true
139
142
  });
143
+ const createAITokenUsageCollection = ()=>({
144
+ slug: tokenUsageCollectionSlug,
145
+ access: {
146
+ create: ()=>false,
147
+ delete: ()=>false,
148
+ read: ()=>false,
149
+ update: ()=>false
150
+ },
151
+ admin: {
152
+ hidden: true
153
+ },
154
+ fields: [
155
+ {
156
+ name: "userID",
157
+ type: "text",
158
+ index: true,
159
+ required: true
160
+ },
161
+ {
162
+ name: "provider",
163
+ type: "text",
164
+ required: true
165
+ },
166
+ {
167
+ name: "model",
168
+ type: "text",
169
+ required: true
170
+ },
171
+ {
172
+ name: "inputTokens",
173
+ type: "number"
174
+ },
175
+ {
176
+ name: "outputTokens",
177
+ type: "number"
178
+ },
179
+ {
180
+ name: "totalTokens",
181
+ type: "number",
182
+ required: true
183
+ },
184
+ {
185
+ name: "recordedAt",
186
+ type: "date",
187
+ index: true,
188
+ required: true
189
+ }
190
+ ],
191
+ timestamps: true
192
+ });
140
193
  const addAccountFields = ({ allowUserApiKeys, config })=>{
141
194
  const adminUserSlug = config.admin?.user;
142
195
  if (!adminUserSlug || !config.collections) return;
@@ -163,22 +216,87 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
163
216
  });
164
217
  }
165
218
  };
219
+ const aiField = {
220
+ name: "payloadAi",
221
+ type: "ui",
222
+ admin: {
223
+ components: {
224
+ Field: "@mvriu5/payload-ai/client#AIInput"
225
+ }
226
+ }
227
+ };
228
+ const getEntityLabel = (label, fallback)=>{
229
+ if (typeof label === "string") return label;
230
+ if (label && typeof label === "object") {
231
+ const translatedLabel = Object.values(label).find((value)=>typeof value === "string");
232
+ if (typeof translatedLabel === "string") return translatedLabel;
233
+ }
234
+ return fallback;
235
+ };
236
+ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, config })=>{
237
+ const pageContexts = new Map();
238
+ for (const collection of config.collections || []){
239
+ if (isInternalCollection(collection.slug)) continue;
240
+ if (collection.slug === "payload-ai-auditlog") continue;
241
+ if (addGenerateFields) {
242
+ const pageContext = addTextGenerationFields({
243
+ fields: collection.fields || [],
244
+ label: getEntityLabel(collection.labels?.singular, collection.slug),
245
+ slug: collection.slug,
246
+ type: "collection"
247
+ });
248
+ pageContexts.set(`collection:${collection.slug}`, pageContext);
249
+ }
250
+ if (addAIInput) collection.fields = [
251
+ aiField,
252
+ ...collection.fields || []
253
+ ];
254
+ }
255
+ for (const global of config.globals || []){
256
+ if (addGenerateFields) {
257
+ const pageContext = addTextGenerationFields({
258
+ fields: global.fields || [],
259
+ label: getEntityLabel(global.label, global.slug),
260
+ slug: global.slug,
261
+ type: "global"
262
+ });
263
+ pageContexts.set(`global:${global.slug}`, pageContext);
264
+ }
265
+ if (addAIInput) global.fields = [
266
+ aiField,
267
+ ...global.fields || []
268
+ ];
269
+ }
270
+ return pageContexts;
271
+ };
166
272
  export const payloadAiPlugin = (pluginOptions)=>(config)=>{
167
273
  const incomingOnInit = config.onInit;
168
274
  const collectionPermissions = resolveCollectionPermissions(pluginOptions.collections);
169
- const allowUserApiKeys = pluginOptions.allowUserApiKeys !== false;
275
+ const providerConfigs = resolveAIProviderConfigs(pluginOptions.providers);
276
+ const managedProviders = providerConfigs.length > 0;
277
+ const allowUserApiKeys = !managedProviders && pluginOptions.allowUserApiKeys !== false;
170
278
  const modelConfig = getResolvedAIModelConfig(pluginOptions.models);
279
+ const maxTokenUsage = resolveMaxTokenUsageOptions(pluginOptions.maxTokenUsage);
171
280
  const mediaUploadOptions = resolveMediaUploadOptions(pluginOptions.media);
172
281
  const maxOutputTokens = typeof pluginOptions.maxOutputTokens === "number" && Number.isFinite(pluginOptions.maxOutputTokens) && pluginOptions.maxOutputTokens > 0 ? Math.floor(pluginOptions.maxOutputTokens) : undefined;
173
282
  if (!config.collections) config.collections = [];
174
283
  if (!config.collections.some((collection)=>collection.slug === "payload-ai-auditlog")) {
175
284
  config.collections.push(createAIChangesCollection());
176
285
  }
177
- addAccountFields({
286
+ if (maxTokenUsage && !config.collections.some((collection)=>collection.slug === tokenUsageCollectionSlug)) {
287
+ config.collections.push(createAITokenUsageCollection());
288
+ }
289
+ if (!managedProviders) addAccountFields({
178
290
  allowUserApiKeys,
179
291
  config
180
292
  });
181
293
  if (pluginOptions.disabled) return config;
294
+ const generateFields = pluginOptions.generateFields !== false;
295
+ const textGenerationPageContexts = addAIFieldsToDocumentsAndGlobals({
296
+ addGenerateFields: generateFields,
297
+ addAIInput: pluginOptions.aiInput !== false,
298
+ config
299
+ });
182
300
  const mentionCollectionSlugs = config.collections.flatMap((collection)=>{
183
301
  if (isInternalCollection(collection.slug)) return [];
184
302
  if (collectionPermissions && !collectionPermissions[collection.slug]?.read) return [];
@@ -194,13 +312,15 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
194
312
  ...config.admin.custom?.payloadAiPlugin || {},
195
313
  collectionSlugs: mentionCollectionSlugs,
196
314
  allowUserApiKeys,
315
+ managedProviders,
197
316
  ...mediaUploadOptions ? {
198
317
  media: {
199
318
  ...mediaUploadOptions,
200
319
  enabled: true
201
320
  }
202
321
  } : {},
203
- models: modelConfig
322
+ models: modelConfig,
323
+ providers: toClientAIProviderProfiles(providerConfigs)
204
324
  }
205
325
  };
206
326
  if (!config.admin.components) config.admin.components = {};
@@ -211,7 +331,9 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
211
331
  allowUserApiKeys,
212
332
  collections: collectionPermissions,
213
333
  maxOutputTokens,
214
- models: modelConfig
334
+ maxTokenUsage,
335
+ models: modelConfig,
336
+ providers: providerConfigs
215
337
  }),
216
338
  method: "post",
217
339
  path: "/ai-chat"
@@ -245,6 +367,20 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
245
367
  method: "post",
246
368
  path: "/ai-mention-suggestion"
247
369
  });
370
+ if (generateFields) {
371
+ config.endpoints.push({
372
+ handler: createGenerateFieldHandler({
373
+ allowUserApiKeys,
374
+ maxOutputTokens,
375
+ maxTokenUsage,
376
+ models: modelConfig,
377
+ pageContexts: textGenerationPageContexts,
378
+ providers: providerConfigs
379
+ }),
380
+ method: "post",
381
+ path: "/ai-generate-field"
382
+ });
383
+ }
248
384
  if (mediaUploadOptions) {
249
385
  config.endpoints.push({
250
386
  handler: createMediaUploadHandler(mediaUploadOptions),
@@ -0,0 +1,26 @@
1
+ import type { Field } from "payload";
2
+ type GeneratableField = Extract<Field, {
3
+ type: "json" | "richText" | "text" | "textarea";
4
+ }>;
5
+ export type TextGenerationFieldContext = {
6
+ description?: string;
7
+ fieldType: GeneratableField["type"];
8
+ hasMany: boolean;
9
+ key: string;
10
+ label: string;
11
+ maxLength?: number;
12
+ name: string;
13
+ };
14
+ export type TextGenerationPageContext = {
15
+ fields: TextGenerationFieldContext[];
16
+ label: string;
17
+ slug: string;
18
+ type: "collection" | "global";
19
+ };
20
+ export declare const addTextGenerationFields: ({ fields, label, slug, type, }: {
21
+ fields: Field[];
22
+ label: string;
23
+ slug: string;
24
+ type: TextGenerationPageContext["type"];
25
+ }) => TextGenerationPageContext;
26
+ export {};
@@ -0,0 +1,99 @@
1
+ const generateFieldComponent = "@mvriu5/payload-ai/client#GenerateField";
2
+ const getLabel = (field)=>{
3
+ if (typeof field.label === "string") return field.label;
4
+ if (field.label && typeof field.label === "object") {
5
+ const label = Object.values(field.label).find((value)=>typeof value === "string");
6
+ if (typeof label === "string") return label;
7
+ }
8
+ return field.name;
9
+ };
10
+ const getDescription = (field)=>{
11
+ const description = field.admin?.description;
12
+ return typeof description === "string" ? description : undefined;
13
+ };
14
+ const addGenerateComponent = (field, key)=>{
15
+ field.admin = field.admin || {};
16
+ field.admin.components = field.admin.components || {};
17
+ const current = field.admin.components.afterInput || [];
18
+ const alreadyAdded = current.some((component)=>{
19
+ if (typeof component === "string") return component === generateFieldComponent;
20
+ return component && typeof component === "object" && "path" in component && component.path === generateFieldComponent;
21
+ });
22
+ if (alreadyAdded) return;
23
+ field.admin.components.afterInput = [
24
+ ...current,
25
+ {
26
+ clientProps: {
27
+ generationFieldKey: key,
28
+ generationFieldType: field.type
29
+ },
30
+ path: generateFieldComponent
31
+ }
32
+ ];
33
+ };
34
+ const visitFields = ({ fields, parentKey, result })=>{
35
+ for (const field of fields){
36
+ if (field.type === "tabs") {
37
+ for (const tab of field.tabs){
38
+ visitFields({
39
+ fields: tab.fields,
40
+ parentKey: "name" in tab ? `${parentKey}.${tab.name}` : parentKey,
41
+ result
42
+ });
43
+ }
44
+ continue;
45
+ }
46
+ if (field.type === "blocks") {
47
+ for (const block of field.blocks){
48
+ if (typeof block !== "object") continue;
49
+ visitFields({
50
+ fields: block.fields,
51
+ parentKey: `${parentKey}.${field.name}.${block.slug}`,
52
+ result
53
+ });
54
+ }
55
+ continue;
56
+ }
57
+ if ("fields" in field && Array.isArray(field.fields)) {
58
+ const nextParent = "name" in field && field.name ? `${parentKey}.${field.name}` : parentKey;
59
+ visitFields({
60
+ fields: field.fields,
61
+ parentKey: nextParent,
62
+ result
63
+ });
64
+ }
65
+ if (![
66
+ "json",
67
+ "richText",
68
+ "text",
69
+ "textarea"
70
+ ].includes(field.type) || !("name" in field) || !field.name) continue;
71
+ const key = `${parentKey}.${field.name}`;
72
+ const generatableField = field;
73
+ if (generatableField.admin?.hidden) continue;
74
+ addGenerateComponent(generatableField, key);
75
+ result.push({
76
+ description: getDescription(generatableField),
77
+ fieldType: generatableField.type,
78
+ hasMany: "hasMany" in generatableField ? Boolean(generatableField.hasMany) : false,
79
+ key,
80
+ label: getLabel(generatableField),
81
+ maxLength: "maxLength" in generatableField ? generatableField.maxLength : undefined,
82
+ name: generatableField.name
83
+ });
84
+ }
85
+ };
86
+ export const addTextGenerationFields = ({ fields, label, slug, type })=>{
87
+ const result = [];
88
+ visitFields({
89
+ fields,
90
+ parentKey: slug,
91
+ result
92
+ });
93
+ return {
94
+ fields: result,
95
+ label,
96
+ slug,
97
+ type
98
+ };
99
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mvriu5/payload-ai",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
4
4
  "description": "AI assistant plugin for Payload CMS with provider selection, CMS mentions, and signed action proposals.",
5
5
  "keywords": [
6
6
  "payload",
@@ -68,16 +68,17 @@
68
68
  "devDependencies": {
69
69
  "@ai-sdk/anthropic": "^3.0.81",
70
70
  "@ai-sdk/google": "^3.0.80",
71
- "@ai-sdk/mistral": "^3.0.37",
71
+ "@ai-sdk/mistral": "^4.0.13",
72
72
  "@ai-sdk/openai": "^3.0.67",
73
- "@openrouter/ai-sdk-provider": "^2.9.1",
74
- "@payloadcms/db-postgres": "3.84.1",
75
- "@payloadcms/next": "3.84.1",
76
- "@payloadcms/richtext-lexical": "3.84.1",
77
- "@payloadcms/ui": "3.84.1",
73
+ "@floating-ui/react": "0.27.20",
74
+ "@openrouter/ai-sdk-provider": "^3.0.0",
75
+ "@payloadcms/db-postgres": "3.86.0",
76
+ "@payloadcms/next": "3.86.0",
77
+ "@payloadcms/richtext-lexical": "3.86.0",
78
+ "@payloadcms/ui": "3.86.0",
78
79
  "@playwright/test": "^1.56.1",
79
80
  "@swc/cli": "0.8.1",
80
- "@types/node": "25.9.3",
81
+ "@types/node": "26.1.1",
81
82
  "@types/react": "19.2.17",
82
83
  "@types/react-dom": "19.2.3",
83
84
  "copyfiles": "2.4.1",
@@ -86,22 +87,23 @@
86
87
  "graphql": "^17.0.1",
87
88
  "jsdom": "^29.1.1",
88
89
  "knip": "^6.18.0",
89
- "next": "16.2.9",
90
- "payload": "3.84.1",
90
+ "next": "16.2.11",
91
+ "payload": "3.86.0",
91
92
  "prettier": "^3.8.4",
92
93
  "react": "19.2.7",
93
94
  "react-dom": "19.2.7",
94
95
  "rimraf": "6.1.3",
95
- "sharp": "0.35.1",
96
- "typescript": "5.9.3",
97
- "vitest": "4.1.9"
96
+ "sharp": "0.35.3",
97
+ "typescript": "6.0.3",
98
+ "vitest": "4.1.10"
98
99
  },
99
100
  "peerDependencies": {
100
101
  "@ai-sdk/anthropic": "^3.0.81",
101
102
  "@ai-sdk/google": "^3.0.80",
102
- "@ai-sdk/mistral": "^3.0.37",
103
+ "@ai-sdk/mistral": "^4.0.13",
103
104
  "@ai-sdk/openai": "^3.0.67",
104
- "@openrouter/ai-sdk-provider": "^2.9.1",
105
+ "@openrouter/ai-sdk-provider": "^3.0.0",
106
+ "@payloadcms/ui": "^3.84.1",
105
107
  "payload": "^3.84.1"
106
108
  },
107
109
  "peerDependenciesMeta": {
@@ -147,7 +149,7 @@
147
149
  ]
148
150
  },
149
151
  "dependencies": {
150
- "ai": "^6.0.193",
152
+ "ai": "^7.0.31",
151
153
  "zod": "^4.4.3"
152
154
  }
153
155
  }