@mvriu5/payload-ai 0.5.9 → 0.5.10

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 (69) hide show
  1. package/README.md +62 -6
  2. package/dist/ai/{proposals.d.ts → proposalSigning.d.ts} +1 -1
  3. package/dist/ai/{proposals.js → proposalSigning.js} +6 -11
  4. package/dist/ai/providerOptions.d.ts +17 -1
  5. package/dist/ai/providerOptions.js +35 -7
  6. package/dist/ai/providerRuntime.d.ts +2 -2
  7. package/dist/ai/providerRuntime.js +50 -16
  8. package/dist/ai/sensitiveData.js +2 -6
  9. package/dist/components/AIInput.d.ts +2 -1
  10. package/dist/components/AIInput.js +210 -39
  11. package/dist/components/AIInput.module.css +15 -1
  12. package/dist/components/APIKeyField.d.ts +3 -0
  13. package/dist/components/{AIApiKeyField.js → APIKeyField.js} +3 -4
  14. package/dist/components/ActionToast.d.ts +30 -0
  15. package/dist/components/{AIActionProposalList.js → ActionToast.js} +10 -8
  16. package/dist/components/AuditLogList.d.ts +32 -0
  17. package/dist/components/{RecentChangesList.js → AuditLogList.js} +13 -12
  18. package/dist/components/{RecentChangesList.module.css → AuditLogList.module.css} +18 -1
  19. package/dist/components/DiffDialog.d.ts +10 -3
  20. package/dist/components/DiffDialog.js +164 -78
  21. package/dist/components/DiffDialog.module.css +86 -2
  22. package/dist/components/Icons.d.ts +5 -0
  23. package/dist/components/Icons.js +109 -3
  24. package/dist/components/MentionPopover.d.ts +19 -0
  25. package/dist/components/{CollectionMentionPopover.js → MentionPopover.js} +11 -4
  26. package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
  27. package/dist/components/hooks/useAISettings.d.ts +6 -4
  28. package/dist/components/hooks/useAISettings.js +18 -3
  29. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
  30. package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
  31. package/dist/exports/client.d.ts +4 -2
  32. package/dist/exports/client.js +4 -2
  33. package/dist/handlers/applyActionHandler.d.ts +8 -0
  34. package/dist/handlers/applyActionHandler.js +563 -0
  35. package/dist/handlers/auditLogHandler.d.ts +6 -0
  36. package/dist/{endpoints/aiRecentChangesEndpointHandler.js → handlers/auditLogHandler.js} +12 -8
  37. package/dist/handlers/chatHandler.d.ts +42 -0
  38. package/dist/handlers/chatHandler.js +634 -0
  39. package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
  40. package/dist/handlers/mentionSuggestionHandler.js +97 -0
  41. package/dist/handlers/proposalDiffHandler.d.ts +7 -0
  42. package/dist/handlers/proposalDiffHandler.js +208 -0
  43. package/dist/index.d.ts +6 -6
  44. package/dist/index.js +50 -18
  45. package/dist/payload/collectionPermissions.d.ts +14 -13
  46. package/dist/payload/collectionPermissions.js +3 -2
  47. package/dist/payload/normalizeData.d.ts +8 -0
  48. package/dist/payload/normalizeData.js +25 -3
  49. package/dist/payload/schemaContext.d.ts +43 -16
  50. package/dist/payload/schemaContext.js +139 -18
  51. package/dist/payload/shared.d.ts +33 -0
  52. package/dist/payload/shared.js +114 -0
  53. package/package.json +7 -2
  54. package/dist/components/AIActionProposalList.d.ts +0 -26
  55. package/dist/components/AIApiKeyField.d.ts +0 -2
  56. package/dist/components/CollectionMentionPopover.d.ts +0 -16
  57. package/dist/components/RecentChangesList.d.ts +0 -18
  58. package/dist/components/hooks/utils.d.ts +0 -1
  59. package/dist/components/hooks/utils.js +0 -3
  60. package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -8
  61. package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -365
  62. package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
  63. package/dist/endpoints/aiChatEndpointHandler.js +0 -382
  64. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
  65. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +0 -63
  66. package/dist/endpoints/aiProposalDiffEndpointHandler.d.ts +0 -7
  67. package/dist/endpoints/aiProposalDiffEndpointHandler.js +0 -138
  68. package/dist/endpoints/aiRecentChangesEndpointHandler.d.ts +0 -6
  69. /package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +0 -0
@@ -1,18 +1,19 @@
1
1
  import type { PayloadHandler } from "payload";
2
- import { type ResolvedAICollectionPermissionMap } from "./collectionPermissions.js";
3
- export type AIChatMention = {
2
+ import { type ResolvedCollectionPermissionMap } from "./collectionPermissions.js";
3
+ export type ChatMention = {
4
4
  collection?: string;
5
5
  id?: string;
6
6
  label?: string;
7
7
  parent?: string;
8
8
  slug?: string;
9
- type?: "block" | "collection" | "doc" | "global";
9
+ type?: "block" | "collection" | "doc" | "global" | "locale";
10
10
  };
11
11
  export type FieldConfig = {
12
12
  admin?: {
13
13
  condition?: unknown;
14
14
  };
15
15
  blocks?: BlockConfig[];
16
+ defaultValue?: unknown;
16
17
  fields?: FieldConfig[];
17
18
  hasMany?: boolean;
18
19
  label?: unknown;
@@ -38,6 +39,9 @@ type VersionConfig = boolean | {
38
39
  drafts?: boolean | Record<string, unknown>;
39
40
  };
40
41
  type CollectionLikeConfig = {
42
+ admin?: {
43
+ useAsTitle?: string;
44
+ };
41
45
  access?: Record<string, unknown>;
42
46
  auth?: unknown;
43
47
  fields?: FieldConfig[];
@@ -55,41 +59,64 @@ type MentionContext = {
55
59
  slug: string;
56
60
  })[];
57
61
  collectionSlugs: string[];
58
- collections?: ResolvedAICollectionPermissionMap;
62
+ collections?: ResolvedCollectionPermissionMap;
59
63
  globalSlugs: string[];
60
- mentions?: AIChatMention[];
64
+ mentions?: ChatMention[];
61
65
  req: Parameters<PayloadHandler>[0];
62
66
  };
63
- export declare const describeCollectionLikeConfig: ({ config, permissions, type, }: {
67
+ export declare const describeCollectionLikeSummary: ({ config, permissions, type, }: {
64
68
  config: CollectionLikeConfig;
65
- permissions?: ResolvedAICollectionPermissionMap;
69
+ permissions?: ResolvedCollectionPermissionMap;
66
70
  type: "collection" | "global";
67
71
  }) => {
68
72
  access: {
69
- aiPermissions: Record<import("./collectionPermissions.js").AICollectionAction, boolean> | {
73
+ aiPermissions: Record<import("./collectionPermissions.js").CollectionAction, boolean> | {
70
74
  read: boolean;
71
75
  update: boolean;
72
76
  };
73
77
  hasPayloadAccessControl: boolean;
74
78
  };
79
+ hasAuth: boolean | undefined;
80
+ hasDrafts: boolean | undefined;
81
+ hasLocalizedFields: boolean | undefined;
82
+ label: unknown;
83
+ localizedFieldNames: string[] | undefined;
84
+ slug: string;
85
+ type: "collection" | "global";
86
+ useAsTitle: string | undefined;
87
+ };
88
+ export declare const describeCollectionLikeConfig: ({ config, permissions, type, }: {
89
+ config: CollectionLikeConfig;
90
+ permissions?: ResolvedCollectionPermissionMap;
91
+ type: "collection" | "global";
92
+ }) => {
75
93
  fields: Record<string, unknown>[];
76
- hasAuth: true | undefined;
77
- hasDrafts: true | undefined;
78
- label: {};
94
+ access: {
95
+ aiPermissions: Record<import("./collectionPermissions.js").CollectionAction, boolean> | {
96
+ read: boolean;
97
+ update: boolean;
98
+ };
99
+ hasPayloadAccessControl: boolean;
100
+ };
101
+ hasAuth: boolean | undefined;
102
+ hasDrafts: boolean | undefined;
103
+ hasLocalizedFields: boolean | undefined;
104
+ label: unknown;
105
+ localizedFieldNames: string[] | undefined;
79
106
  slug: string;
80
107
  type: "collection" | "global";
108
+ useAsTitle: string | undefined;
81
109
  };
82
- export declare const describeField: (field: FieldConfig) => Record<string, unknown>;
83
- export declare const collectBlocks: ({ fields, parent, }: {
110
+ export declare const collectBlocks: ({ fields, parent }: {
84
111
  fields: FieldConfig[];
85
112
  parent: string;
86
113
  }) => (Record<string, unknown> & {
87
114
  parent: string;
88
115
  slug: string;
89
116
  })[];
90
- export declare const getAllowedCollectionSlugs: (req: Parameters<PayloadHandler>[0], collections?: ResolvedAICollectionPermissionMap) => string[];
91
- export declare const getMentionContext: ({ blockContexts, collectionSlugs, collections, globalSlugs, mentions, req, }: MentionContext) => Promise<Record<string, unknown>[]>;
92
- export declare const buildPromptWithMentionContext: ({ mentionContext, prompt, }: {
117
+ export declare const getAllowedCollectionSlugs: (req: Parameters<PayloadHandler>[0], collections?: ResolvedCollectionPermissionMap) => string[];
118
+ export declare const getMentionContext: ({ blockContexts, collectionSlugs, collections, globalSlugs, mentions, req }: MentionContext) => Promise<Record<string, unknown>[]>;
119
+ export declare const buildPromptWithMentionContext: ({ mentionContext, prompt }: {
93
120
  mentionContext: Record<string, unknown>[];
94
121
  prompt: string;
95
122
  }) => string;
@@ -1,15 +1,47 @@
1
1
  import { getCollectionPermissions, getCollectionSlugsForAction } from "./collectionPermissions.js";
2
2
  import { getCollectionFields, isAuthCollection } from "./normalizeData.js";
3
- import { getSerializableLabel, isInternalCollection } from "./shared.js";
3
+ import { getSerializableLabel, isInternalCollection, isRecord } from "./shared.js";
4
4
  const getSerializableRelationTo = (relationTo)=>{
5
- if (typeof relationTo === "string") {
6
- return relationTo;
7
- }
5
+ if (typeof relationTo === "string") return relationTo;
8
6
  if (Array.isArray(relationTo) && relationTo.every((item)=>typeof item === "string")) {
9
7
  return relationTo;
10
8
  }
11
9
  return undefined;
12
10
  };
11
+ const fieldDescriptionCache = new WeakMap();
12
+ const blockDescriptionCache = new WeakMap();
13
+ const collectionDescriptionCache = new WeakMap();
14
+ const getLocaleConfigs = (req)=>{
15
+ const localization = req.payload.config.localization;
16
+ const defaultLocale = localization?.defaultLocale;
17
+ const locales = localization?.locales || [];
18
+ return locales.flatMap((locale)=>{
19
+ if (typeof locale === "string") {
20
+ return [
21
+ {
22
+ code: locale,
23
+ ...locale === defaultLocale ? {
24
+ isDefault: true
25
+ } : {},
26
+ label: locale
27
+ }
28
+ ];
29
+ }
30
+ if (!isRecord(locale)) return [];
31
+ const code = "code" in locale && typeof locale.code === "string" ? locale.code : "label" in locale && typeof locale.label === "string" ? locale.label : undefined;
32
+ if (!code) return [];
33
+ const label = "label" in locale ? getSerializableLabel(locale.label, code) : code;
34
+ return [
35
+ {
36
+ code,
37
+ ...code === defaultLocale ? {
38
+ isDefault: true
39
+ } : {},
40
+ label
41
+ }
42
+ ];
43
+ });
44
+ };
13
45
  const getSerializableOptions = (options)=>{
14
46
  if (!Array.isArray(options)) return undefined;
15
47
  const serializedOptions = options.map((option)=>{
@@ -19,7 +51,7 @@ const getSerializableOptions = (options)=>{
19
51
  value: option
20
52
  };
21
53
  }
22
- if (!option || typeof option !== "object") return null;
54
+ if (!isRecord(option)) return null;
23
55
  const label = "label" in option ? getSerializableLabel(option.label) : undefined;
24
56
  const value = "value" in option && typeof option.value === "string" ? option.value : undefined;
25
57
  if (!label && !value) return null;
@@ -37,7 +69,7 @@ const getSerializableOptions = (options)=>{
37
69
  const hasDrafts = (config)=>{
38
70
  const versions = config?.versions;
39
71
  if (!versions || versions === true) return false;
40
- if (typeof versions !== "object") return false;
72
+ if (!isRecord(versions)) return false;
41
73
  return Boolean(versions.drafts);
42
74
  };
43
75
  const toNormalizeCollectionConfig = (config)=>{
@@ -67,8 +99,46 @@ const getSchemaFields = (config)=>{
67
99
  }
68
100
  return fields;
69
101
  };
70
- export const describeCollectionLikeConfig = ({ config, permissions, type })=>{
102
+ const getLocalizedFieldNames = (fields, path = "")=>{
103
+ return fields.flatMap((field)=>{
104
+ const fieldPath = field.name ? path ? `${path}.${field.name}` : field.name : path;
105
+ const nestedFields = field.fields ? getLocalizedFieldNames(field.fields, fieldPath) : [];
106
+ const nestedBlocks = field.blocks ? field.blocks.flatMap((block)=>getLocalizedFieldNames(block.fields || [], `${fieldPath}.${block.slug}`)) : [];
107
+ return [
108
+ ...field.localized && fieldPath ? [
109
+ fieldPath
110
+ ] : [],
111
+ ...nestedFields,
112
+ ...nestedBlocks
113
+ ];
114
+ });
115
+ };
116
+ const getCachedCollectionDescription = (config, type)=>{
117
+ const cached = collectionDescriptionCache.get(config);
118
+ if (cached) return cached;
71
119
  const slug = config.slug;
120
+ const fields = getSchemaFields(config);
121
+ const localizedFieldNames = getLocalizedFieldNames(fields);
122
+ const description = {
123
+ fields: fields.map(describeField),
124
+ hasAuth: isAuthCollection(toNormalizeCollectionConfig(config)) || undefined,
125
+ hasDrafts: hasDrafts(config) || undefined,
126
+ hasLocalizedFields: localizedFieldNames.length > 0 || undefined,
127
+ label: type === "collection" ? config.labels?.plural || config.labels?.singular || slug : getSerializableLabel(config.label) || slug,
128
+ ...localizedFieldNames.length > 0 ? {
129
+ localizedFieldNames
130
+ } : {},
131
+ ...type === "collection" && config.admin?.useAsTitle ? {
132
+ useAsTitle: config.admin.useAsTitle
133
+ } : {},
134
+ slug
135
+ };
136
+ collectionDescriptionCache.set(config, description);
137
+ return description;
138
+ };
139
+ export const describeCollectionLikeSummary = ({ config, permissions, type })=>{
140
+ const description = getCachedCollectionDescription(config, type);
141
+ const slug = description.slug;
72
142
  return {
73
143
  access: {
74
144
  aiPermissions: type === "collection" ? getCollectionPermissions({
@@ -80,19 +150,23 @@ export const describeCollectionLikeConfig = ({ config, permissions, type })=>{
80
150
  },
81
151
  hasPayloadAccessControl: Boolean(config.access)
82
152
  },
83
- fields: getSchemaFields(config).map(describeField),
84
- hasAuth: isAuthCollection(toNormalizeCollectionConfig(config)) || undefined,
85
- hasDrafts: hasDrafts(config) || undefined,
86
- label: type === "collection" ? config.labels?.plural || config.labels?.singular || slug : getSerializableLabel(config.label) || slug,
153
+ hasAuth: description.hasAuth,
154
+ hasDrafts: description.hasDrafts,
155
+ hasLocalizedFields: description.hasLocalizedFields,
156
+ label: description.label,
157
+ localizedFieldNames: description.localizedFieldNames,
87
158
  slug,
88
- type
159
+ type,
160
+ useAsTitle: description.useAsTitle
89
161
  };
90
162
  };
91
- export const describeField = (field)=>{
163
+ const describeField = (field)=>{
164
+ const cached = fieldDescriptionCache.get(field);
165
+ if (cached) return cached;
92
166
  const label = getSerializableLabel(field.label);
93
167
  const relationTo = getSerializableRelationTo(field.relationTo);
94
168
  const options = getSerializableOptions(field.options);
95
- return {
169
+ const description = {
96
170
  ...label ? {
97
171
  label
98
172
  } : {},
@@ -127,13 +201,29 @@ export const describeField = (field)=>{
127
201
  blocks: field.blocks.map(describeBlock)
128
202
  } : {}
129
203
  };
204
+ fieldDescriptionCache.set(field, description);
205
+ return description;
130
206
  };
131
207
  const describeBlock = (block)=>{
132
- return {
208
+ const cached = blockDescriptionCache.get(block);
209
+ if (cached) return cached;
210
+ const description = {
133
211
  fields: (block.fields || []).map(describeField),
134
212
  label: getSerializableLabel(block.labels?.singular) || block.slug,
135
213
  slug: block.slug
136
214
  };
215
+ blockDescriptionCache.set(block, description);
216
+ return description;
217
+ };
218
+ export const describeCollectionLikeConfig = ({ config, permissions, type })=>{
219
+ return {
220
+ ...describeCollectionLikeSummary({
221
+ config,
222
+ permissions,
223
+ type
224
+ }),
225
+ fields: getCachedCollectionDescription(config, type).fields
226
+ };
137
227
  };
138
228
  export const collectBlocks = ({ fields, parent })=>{
139
229
  const blocks = [];
@@ -171,7 +261,30 @@ export const getMentionContext = async ({ blockContexts, collectionSlugs, collec
171
261
  if (!mentions || mentions.length === 0) return [];
172
262
  const context = [];
173
263
  const seen = new Set();
264
+ const localeConfigs = getLocaleConfigs(req);
265
+ const selectedLocales = (mentions.filter((mention)=>mention.type === "locale" && mention.slug).map((mention)=>mention.slug) || []).filter((locale, index, array)=>array.indexOf(locale) === index);
266
+ const activeLocale = selectedLocales.at(-1);
267
+ if (localeConfigs.length > 0) {
268
+ context.push({
269
+ activeLocale,
270
+ defaultLocale: localeConfigs.find((locale)=>locale.isDefault)?.code,
271
+ locales: localeConfigs,
272
+ selectedLocales,
273
+ type: "locales"
274
+ });
275
+ }
174
276
  for (const mention of mentions.slice(0, 8)){
277
+ if (mention.type === "locale" && mention.slug) {
278
+ const locale = localeConfigs.find((item)=>item.code === mention.slug);
279
+ if (!locale) continue;
280
+ const key = `locale:${locale.code}`;
281
+ if (seen.has(key)) continue;
282
+ seen.add(key);
283
+ context.push({
284
+ ...locale,
285
+ type: "locale"
286
+ });
287
+ }
175
288
  if (mention.type === "collection" && mention.slug) {
176
289
  const slug = mention.slug;
177
290
  const key = `collection:${slug}`;
@@ -193,6 +306,9 @@ export const getMentionContext = async ({ blockContexts, collectionSlugs, collec
193
306
  if (!globalConfig) continue;
194
307
  const globalDoc = await req.payload.findGlobal({
195
308
  depth: 2,
309
+ ...activeLocale ? {
310
+ locale: activeLocale
311
+ } : {},
196
312
  overrideAccess: false,
197
313
  req,
198
314
  slug: slug
@@ -226,6 +342,9 @@ export const getMentionContext = async ({ blockContexts, collectionSlugs, collec
226
342
  collection: slug,
227
343
  depth: 2,
228
344
  id: mention.id,
345
+ ...activeLocale ? {
346
+ locale: activeLocale
347
+ } : {},
229
348
  overrideAccess: false,
230
349
  req
231
350
  }).catch(()=>null);
@@ -236,6 +355,9 @@ export const getMentionContext = async ({ blockContexts, collectionSlugs, collec
236
355
  doc,
237
356
  id: mention.id,
238
357
  label: mention.label || mention.id,
358
+ ...activeLocale ? {
359
+ locale: activeLocale
360
+ } : {},
239
361
  type: "doc"
240
362
  });
241
363
  }
@@ -245,9 +367,8 @@ export const getMentionContext = async ({ blockContexts, collectionSlugs, collec
245
367
  export const buildPromptWithMentionContext = ({ mentionContext, prompt })=>{
246
368
  if (mentionContext.length === 0) return prompt;
247
369
  return [
248
- "The user selected the following Payload CMS references in the input. Treat inline text like `collection: Name` or `document: Name` as references to this context, not as literal content.",
249
- JSON.stringify(mentionContext, null, 2),
250
- "User request:",
370
+ "Selected Payload references JSON. Treat inline reference labels as these entities:",
371
+ JSON.stringify(mentionContext),
251
372
  prompt
252
373
  ].join("\n\n");
253
374
  };
@@ -1,2 +1,35 @@
1
+ import type { PayloadHandler } from "payload";
2
+ export type LocalizedDataInput = Record<string, Record<string, unknown>>;
3
+ export type AIActionProposalLike = {
4
+ action: "create" | "delete" | "update" | "updateGlobal";
5
+ collection?: string;
6
+ data?: Record<string, unknown>;
7
+ id?: string;
8
+ label: string;
9
+ locale?: string;
10
+ localizedData?: LocalizedDataInput;
11
+ slug?: string;
12
+ };
1
13
  export declare const getSerializableLabel: (label: unknown, fallback?: string) => string;
2
14
  export declare const isInternalCollection: (slug: string) => boolean;
15
+ export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
16
+ export declare const mergeData: (current: Record<string, unknown>, next: Record<string, unknown>) => Record<string, unknown>;
17
+ export declare const isKnownGlobal: (req: Parameters<PayloadHandler>[0], slug: string) => boolean;
18
+ export declare const hasLocalizedData: <Value>(value: Value) => value is Value & {
19
+ localizedData: LocalizedDataInput;
20
+ };
21
+ export declare const getDefaultLocale: (req: Parameters<PayloadHandler>[0]) => string | null;
22
+ export declare const getJSONLineKey: (line: string) => string | null;
23
+ export declare const isActionProposal: (proposal: unknown) => proposal is AIActionProposalLike;
24
+ export declare const getOptionalNumber: (value: unknown) => number | undefined;
25
+ export declare const getNumber: (value: unknown) => number;
26
+ export declare const getString: (value: unknown) => string | null;
27
+ export declare const getDocLabel: (doc: Record<string, unknown>, useAsTitle?: string) => string;
28
+ export declare const getOptionValue: (option?: string | {
29
+ label?: unknown;
30
+ value?: string;
31
+ }) => string | undefined;
32
+ export declare const hasValueAtPath: (data: Record<string, unknown>, path: string) => boolean;
33
+ export declare const getSafeProposalLabel: (label: string) => string;
34
+ export declare const setValueAtPath: (data: Record<string, unknown>, path: string, value: unknown) => void;
35
+ export declare const isAbortError: (err: unknown) => boolean;
@@ -1,3 +1,4 @@
1
+ const maxProposalLabelLength = 90;
1
2
  export const getSerializableLabel = (label, fallback = "")=>{
2
3
  if (typeof label === "string") return label;
3
4
  if (label && typeof label === "object") {
@@ -9,3 +10,116 @@ export const getSerializableLabel = (label, fallback = "")=>{
9
10
  export const isInternalCollection = (slug)=>{
10
11
  return slug.startsWith("payload-") || slug === "plugin-collection";
11
12
  };
13
+ export const isRecord = (value)=>{
14
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
15
+ };
16
+ export const mergeData = (current, next)=>{
17
+ return Object.fromEntries(Object.entries({
18
+ ...current,
19
+ ...next
20
+ }).map(([key, value])=>{
21
+ const currentValue = current[key];
22
+ if (isRecord(currentValue) && isRecord(value)) {
23
+ return [
24
+ key,
25
+ mergeData(currentValue, value)
26
+ ];
27
+ }
28
+ return [
29
+ key,
30
+ value
31
+ ];
32
+ }));
33
+ };
34
+ export const isKnownGlobal = (req, slug)=>{
35
+ return req.payload.config.globals?.some((item)=>item.slug === slug) || false;
36
+ };
37
+ export const hasLocalizedData = (value)=>{
38
+ return isRecord(value) && isRecord(value.localizedData);
39
+ };
40
+ export const getDefaultLocale = (req)=>{
41
+ const localization = req.payload.config.localization;
42
+ if (!localization) return null;
43
+ return localization.defaultLocale || null;
44
+ };
45
+ export const getJSONLineKey = (line)=>{
46
+ const match = /^(\s*)"([^"]+)":/.exec(line);
47
+ return match ? `${match[1]}${match[2]}` : null;
48
+ };
49
+ const isNonEmptyString = (value)=>{
50
+ return typeof value === "string" && value.trim().length > 0;
51
+ };
52
+ const hasWriteData = (proposal)=>{
53
+ const hasData = isRecord(proposal.data);
54
+ const hasLocalizedProposalData = hasLocalizedData(proposal);
55
+ return hasData !== hasLocalizedProposalData;
56
+ };
57
+ export const isActionProposal = (proposal)=>{
58
+ if (!isRecord(proposal) || !isNonEmptyString(proposal.label)) return false;
59
+ if (proposal.action === "create") {
60
+ return isNonEmptyString(proposal.collection) && hasWriteData(proposal);
61
+ }
62
+ if (proposal.action === "update") {
63
+ return isNonEmptyString(proposal.collection) && isNonEmptyString(proposal.id) && hasWriteData(proposal);
64
+ }
65
+ if (proposal.action === "delete") {
66
+ return isNonEmptyString(proposal.collection) && isNonEmptyString(proposal.id);
67
+ }
68
+ if (proposal.action === "updateGlobal") {
69
+ return isNonEmptyString(proposal.slug) && hasWriteData(proposal);
70
+ }
71
+ return false;
72
+ };
73
+ export const getOptionalNumber = (value)=>{
74
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
75
+ };
76
+ export const getNumber = (value)=>{
77
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
78
+ };
79
+ export const getString = (value)=>{
80
+ return typeof value === "string" ? value : null;
81
+ };
82
+ export const getDocLabel = (doc, useAsTitle)=>{
83
+ const titleField = useAsTitle ? getSerializableLabel(doc[useAsTitle]) : "";
84
+ const title = getSerializableLabel(doc.title);
85
+ const name = getSerializableLabel(doc.name);
86
+ const email = getSerializableLabel(doc.email);
87
+ return titleField || title || name || email || doc.id?.toString() || "Untitled";
88
+ };
89
+ export const getOptionValue = (option)=>{
90
+ if (!option) return undefined;
91
+ if (typeof option === "string") return option;
92
+ return typeof option.value === "string" ? option.value : undefined;
93
+ };
94
+ export const hasValueAtPath = (data, path)=>{
95
+ const segments = path.split(".");
96
+ let current = data;
97
+ for (const segment of segments){
98
+ if (!isRecord(current) || current[segment] === undefined) {
99
+ return false;
100
+ }
101
+ current = current[segment];
102
+ }
103
+ return true;
104
+ };
105
+ export const getSafeProposalLabel = (label)=>{
106
+ const firstLine = label.replace(/[*_`>#-]/g, "").split("\n").map((line)=>line.trim()).find(Boolean);
107
+ if (!firstLine) return "Review proposed CMS change";
108
+ if (firstLine.length <= maxProposalLabelLength) return firstLine;
109
+ return `${firstLine.slice(0, maxProposalLabelLength - 3).trim()}...`;
110
+ };
111
+ export const setValueAtPath = (data, path, value)=>{
112
+ const segments = path.split(".");
113
+ let current = data;
114
+ for (const segment of segments.slice(0, -1)){
115
+ const next = current[segment];
116
+ if (!isRecord(next)) {
117
+ current[segment] = {};
118
+ }
119
+ current = current[segment];
120
+ }
121
+ current[segments[segments.length - 1]] = value;
122
+ };
123
+ export const isAbortError = (err)=>{
124
+ return err instanceof DOMException && err.name === "AbortError";
125
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mvriu5/payload-ai",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "description": "AI assistant plugin for Payload CMS with provider selection, CMS mentions, and signed action proposals.",
5
5
  "keywords": [
6
6
  "payload",
@@ -75,7 +75,7 @@
75
75
  "payload": "3.84.1",
76
76
  "prettier": "^3.8.4",
77
77
  "react": "19.2.7",
78
- "react-dom": "19.2.6",
78
+ "react-dom": "19.2.7",
79
79
  "rimraf": "6.1.3",
80
80
  "sharp": "0.35.1",
81
81
  "typescript": "5.9.3",
@@ -94,6 +94,7 @@
94
94
  },
95
95
  "knip": {
96
96
  "entry": [
97
+ "src/exports/client.ts",
97
98
  "dev/payload.config.ts",
98
99
  "dev/next.config.mjs",
99
100
  "dev/app/**/*.{ts,tsx}"
@@ -101,6 +102,9 @@
101
102
  "ignore": [
102
103
  "dev/payload-types.ts"
103
104
  ],
105
+ "ignoreDependencies": [
106
+ "prettier"
107
+ ],
104
108
  "project": [
105
109
  "src/**/*.{ts,tsx}",
106
110
  "dev/**/*.{ts,tsx,mjs}"
@@ -111,6 +115,7 @@
111
115
  "@ai-sdk/google": "^3.0.80",
112
116
  "@ai-sdk/mistral": "^3.0.37",
113
117
  "@ai-sdk/openai": "^3.0.67",
118
+ "@openrouter/ai-sdk-provider": "^2.9.1",
114
119
  "ai": "^6.0.193",
115
120
  "zod": "^4.4.3"
116
121
  }
@@ -1,26 +0,0 @@
1
- export type AIActionProposal = {
2
- _aiSignature?: {
3
- expiresAt: string;
4
- value: string;
5
- };
6
- action: "create" | "delete" | "update" | "updateGlobal";
7
- collection?: string;
8
- data?: Record<string, unknown>;
9
- id?: string;
10
- label: string;
11
- slug?: string;
12
- };
13
- type AIActionProposalListProps = {
14
- apiRoute: string;
15
- appliedProposalIndexes: number[];
16
- description?: string;
17
- error?: string;
18
- getViewURL?: (proposal: AIActionProposal) => string | null;
19
- isApplying: boolean;
20
- onDismiss?: () => void;
21
- onDismissError?: () => void;
22
- onApply: (proposal: AIActionProposal, index: number) => void;
23
- proposals: AIActionProposal[];
24
- };
25
- export declare const AIActionProposalList: ({ apiRoute, appliedProposalIndexes, description, error, getViewURL, isApplying, onDismiss, onDismissError, onApply, proposals }: AIActionProposalListProps) => import("react").JSX.Element | null;
26
- export {};
@@ -1,2 +0,0 @@
1
- import type { TextFieldClientComponent } from "payload";
2
- export declare const AIApiKeyField: TextFieldClientComponent;
@@ -1,16 +0,0 @@
1
- import type { RefObject } from "react";
2
- export type CollectionMentionOption = {
3
- collection?: string;
4
- id?: string;
5
- label: string;
6
- parent?: string;
7
- slug: string;
8
- type: "block" | "collection" | "doc" | "global";
9
- };
10
- type CollectionMentionPopoverProps = {
11
- containerRef?: RefObject<HTMLDivElement | null>;
12
- suggestions: CollectionMentionOption[];
13
- onSelect: (suggestion: CollectionMentionOption) => void;
14
- };
15
- export declare const CollectionMentionPopover: ({ containerRef, onSelect, suggestions }: CollectionMentionPopoverProps) => import("react").JSX.Element | null;
16
- export {};
@@ -1,18 +0,0 @@
1
- import type { AIActionProposal } from "./AIActionProposalList.js";
2
- export type AppliedChange = {
3
- action?: AIActionProposal["action"] | null;
4
- additions: number;
5
- after?: unknown;
6
- before?: unknown;
7
- collection?: string | null;
8
- documentID?: string | null;
9
- removals: number;
10
- slug?: string | null;
11
- title: string;
12
- url?: string | null;
13
- };
14
- type RecentChangesListProps = {
15
- changes: AppliedChange[];
16
- };
17
- export declare const RecentChangesList: ({ changes }: RecentChangesListProps) => import("react").JSX.Element;
18
- export {};
@@ -1 +0,0 @@
1
- export declare const isAbortError: (err: unknown) => boolean;
@@ -1,3 +0,0 @@
1
- export const isAbortError = (err)=>{
2
- return err instanceof DOMException && err.name === "AbortError";
3
- };
@@ -1,8 +0,0 @@
1
- import type { PayloadHandler } from "payload";
2
- import { type ResolvedAICollectionPermissionMap } from "../payload/collectionPermissions.js";
3
- type AIApplyActionEndpointOptions = {
4
- changeLogCollection?: string;
5
- collections?: ResolvedAICollectionPermissionMap;
6
- };
7
- export declare const createAIApplyActionEndpointHandler: (options?: AIApplyActionEndpointOptions) => PayloadHandler;
8
- export {};