@mvriu5/payload-ai 1.2.0 → 1.3.2

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 (51) hide show
  1. package/README.md +42 -0
  2. package/dist/components/Icons.d.ts +0 -8
  3. package/dist/components/Icons.js +4 -216
  4. package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +1 -2
  5. package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +25 -34
  6. package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
  7. package/dist/components/ai-input/AIInput.js +433 -0
  8. package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +111 -43
  9. package/dist/components/ai-input/badge.d.ts +14 -0
  10. package/dist/components/ai-input/badge.js +85 -0
  11. package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
  12. package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +49 -21
  13. package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
  14. package/dist/components/dashboard/Dashboard.d.ts +2 -0
  15. package/dist/components/dashboard/Dashboard.js +15 -0
  16. package/dist/components/dashboard/Dashboard.module.css +13 -0
  17. package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
  18. package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
  19. package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
  20. package/dist/components/hooks/useAIChatStream.d.ts +39 -0
  21. package/dist/components/hooks/useAIChatStream.js +217 -0
  22. package/dist/components/hooks/useAISettings.js +26 -25
  23. package/dist/components/hooks/useAuditLog.d.ts +11 -0
  24. package/dist/components/hooks/useAuditLog.js +41 -0
  25. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
  26. package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
  27. package/dist/components/hooks/useMentions.d.ts +58 -0
  28. package/dist/components/hooks/useMentions.js +276 -0
  29. package/dist/components/hooks/usePluginConfig.d.ts +52 -0
  30. package/dist/components/hooks/usePluginConfig.js +20 -0
  31. package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
  32. package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
  33. package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
  34. package/dist/exports/client.d.ts +2 -2
  35. package/dist/exports/client.js +2 -2
  36. package/dist/handlers/applyActionHandler.js +27 -7
  37. package/dist/handlers/chatHandler.js +302 -58
  38. package/dist/handlers/mediaUploadHandler.d.ts +7 -0
  39. package/dist/handlers/mediaUploadHandler.js +99 -0
  40. package/dist/handlers/mentionSuggestionHandler.js +16 -14
  41. package/dist/handlers/proposalDiffHandler.js +41 -29
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.js +39 -7
  44. package/dist/payload/collectionPermissions.js +3 -1
  45. package/dist/payload/normalizeData.d.ts +0 -6
  46. package/dist/payload/normalizeData.js +25 -13
  47. package/dist/payload/proposalData.js +4 -1
  48. package/dist/payload/schemaContext.js +98 -43
  49. package/package.json +6 -6
  50. package/dist/components/AIInput.js +0 -896
  51. /package/dist/components/{AIInput.d.ts → ai-input/AIInput.d.ts} +0 -0
@@ -46,7 +46,7 @@ const createDebugPayload = ({ activeLocale, debug, proposalCount, selectedLocale
46
46
  });
47
47
  const createE2EChatResponse = ({ prompt, selectedLocales })=>{
48
48
  const normalizedPrompt = prompt.toLowerCase();
49
- const wantsCreatePost = normalizedPrompt.includes("post") && (normalizedPrompt.includes("create") || normalizedPrompt.includes("erstell"));
49
+ const wantsCreatePost = normalizedPrompt.includes("post") && (normalizedPrompt.includes("create") || normalizedPrompt.includes("erstell") || normalizedPrompt.includes("apply flow") || normalizedPrompt.includes("locale review") || normalizedPrompt.includes("proposal review"));
50
50
  const mentionsMars = normalizedPrompt.includes("mars");
51
51
  const multipleLocales = selectedLocales.length > 1;
52
52
  const activeLocale = selectedLocales.at(-1);
@@ -241,14 +241,23 @@ const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
241
241
  ];
242
242
  }
243
243
  const missing = [];
244
+ const localizedRequiredFields = [];
245
+ const sharedRequiredFields = [];
246
+ for (const field of requiredFields){
247
+ if (field.localized) {
248
+ localizedRequiredFields.push(field);
249
+ } else {
250
+ sharedRequiredFields.push(field);
251
+ }
252
+ }
244
253
  for (const [locale, localeData] of locales){
245
- for (const field of requiredFields.filter((item)=>item.localized)){
254
+ for (const field of localizedRequiredFields){
246
255
  if (!hasValueAtPath(localeData, field.path)) {
247
256
  missing.push(`${locale}:${field.path}`);
248
257
  }
249
258
  }
250
259
  }
251
- for (const field of requiredFields.filter((item)=>!item.localized)){
260
+ for (const field of sharedRequiredFields){
252
261
  if (!hasValueAtPath(firstLocale[1], field.path)) {
253
262
  missing.push(`${firstLocale[0]}:${field.path}`);
254
263
  }
@@ -258,7 +267,9 @@ const getMissingCreateFields = ({ data, localizedData, requiredFields })=>{
258
267
  if (!data) return [
259
268
  "data is required"
260
269
  ];
261
- return requiredFields.filter((field)=>!hasValueAtPath(data, field.path)).map((field)=>field.path);
270
+ return requiredFields.flatMap((field)=>hasValueAtPath(data, field.path) ? [] : [
271
+ field.path
272
+ ]);
262
273
  };
263
274
  const getProposalSummary = (proposal)=>({
264
275
  action: proposal.action,
@@ -293,19 +304,28 @@ const getCollectionBlockTypes = (fields)=>{
293
304
  ...blockTypes
294
305
  ];
295
306
  };
307
+ const regexSpecialCharactersPattern = /[.*+?^${}()|[\]\\]/g;
296
308
  const getRequestedBlockTypes = ({ availableBlockTypes, mentions, prompt })=>{
297
309
  const requestedBlockTypes = new Set();
298
310
  const normalizedPrompt = prompt.toLowerCase();
311
+ const availableBlockTypeSet = new Set(availableBlockTypes);
312
+ const blockTypesByNormalizedSlug = new Map(availableBlockTypes.map((blockType)=>[
313
+ blockType.toLowerCase(),
314
+ blockType
315
+ ]));
316
+ const escapedBlockTypes = availableBlockTypes.map((blockType)=>blockType.replace(regexSpecialCharactersPattern, "\\$&")).join("|");
317
+ const blockPattern = escapedBlockTypes ? new RegExp(`\\b(${escapedBlockTypes})\\b(?:\\s+block)?`, "gi") : null;
299
318
  for (const mention of mentions || []){
300
- if (mention.type === "block" && mention.slug && availableBlockTypes.includes(mention.slug)) {
319
+ if (mention.type === "block" && mention.slug && availableBlockTypeSet.has(mention.slug)) {
301
320
  requestedBlockTypes.add(mention.slug);
302
321
  }
303
322
  }
304
- for (const blockType of availableBlockTypes){
305
- const escapedBlockType = blockType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
306
- const blockPattern = new RegExp(`\\b${escapedBlockType}\\b(?:\\s+block)?`, "i");
307
- if (blockPattern.test(normalizedPrompt)) {
308
- requestedBlockTypes.add(blockType);
323
+ if (blockPattern) {
324
+ for (const match of normalizedPrompt.matchAll(blockPattern)){
325
+ const blockType = match[1] ? blockTypesByNormalizedSlug.get(match[1].toLowerCase()) : null;
326
+ if (blockType) {
327
+ requestedBlockTypes.add(blockType);
328
+ }
309
329
  }
310
330
  }
311
331
  return [
@@ -320,6 +340,10 @@ const collectProposalBlockTypes = ({ data, fields })=>{
320
340
  const fieldValue = value[field.name];
321
341
  if (fieldValue === undefined || fieldValue === null) continue;
322
342
  if (field.type === "blocks" && Array.isArray(fieldValue)) {
343
+ const blocksBySlug = new Map(field.blocks?.map((block)=>[
344
+ block.slug,
345
+ block
346
+ ]));
323
347
  for (const blockItem of fieldValue){
324
348
  if (!isRecord(blockItem)) continue;
325
349
  const blockType = typeof blockItem.blockType === "string" ? blockItem.blockType : typeof blockItem.type === "string" ? blockItem.type : typeof blockItem.slug === "string" ? blockItem.slug : null;
@@ -327,7 +351,7 @@ const collectProposalBlockTypes = ({ data, fields })=>{
327
351
  foundBlockTypes.add(blockType);
328
352
  }
329
353
  if (blockType) {
330
- const blockConfig = field.blocks?.find((candidate)=>candidate.slug === blockType);
354
+ const blockConfig = blocksBySlug.get(blockType);
331
355
  if (blockConfig?.fields?.length) {
332
356
  visitFields(blockConfig.fields, blockItem);
333
357
  }
@@ -410,11 +434,15 @@ const collectRelationshipTargets = ({ data, fields, path = "" })=>{
410
434
  continue;
411
435
  }
412
436
  if (field.type === "blocks" && Array.isArray(fieldValue) && field.blocks?.length) {
437
+ const blocksBySlug = new Map(field.blocks.map((block)=>[
438
+ block.slug,
439
+ block
440
+ ]));
413
441
  fieldValue.forEach((item, index)=>{
414
442
  if (!isRecord(item)) return;
415
443
  const blockType = typeof item.blockType === "string" ? item.blockType : typeof item.type === "string" ? item.type : typeof item.slug === "string" ? item.slug : null;
416
444
  if (!blockType) return;
417
- const blockConfig = field.blocks?.find((candidate)=>candidate.slug === blockType);
445
+ const blockConfig = blocksBySlug.get(blockType);
418
446
  if (!blockConfig?.fields?.length) return;
419
447
  targets.push(...collectRelationshipTargets({
420
448
  data: item,
@@ -426,13 +454,91 @@ const collectRelationshipTargets = ({ data, fields, path = "" })=>{
426
454
  }
427
455
  return targets;
428
456
  };
457
+ const collectUploadTargets = ({ data, fields, path = "" })=>{
458
+ const targets = [];
459
+ for (const field of fields){
460
+ if (!field.name) continue;
461
+ const fieldPath = path ? `${path}.${field.name}` : field.name;
462
+ const fieldValue = data[field.name];
463
+ if (fieldValue === undefined || fieldValue === null) continue;
464
+ if (field.type === "upload") {
465
+ const relationTargets = Array.isArray(field.relationTo) ? field.relationTo.filter((item)=>typeof item === "string") : typeof field.relationTo === "string" ? [
466
+ field.relationTo
467
+ ] : [];
468
+ const collectSingle = (value, itemPath)=>{
469
+ if (typeof value === "string" || typeof value === "number") {
470
+ if (relationTargets.length === 1) {
471
+ targets.push({
472
+ collection: relationTargets[0],
473
+ id: value,
474
+ path: itemPath
475
+ });
476
+ }
477
+ return;
478
+ }
479
+ if (!isRecord(value)) return;
480
+ const relationTo = typeof value.relationTo === "string" ? value.relationTo : relationTargets[0];
481
+ const id = typeof value.id === "string" || typeof value.id === "number" ? value.id : typeof value.value === "string" || typeof value.value === "number" ? value.value : undefined;
482
+ if (!relationTo || id === undefined) return;
483
+ targets.push({
484
+ collection: relationTo,
485
+ id,
486
+ path: itemPath
487
+ });
488
+ };
489
+ if (field.hasMany && Array.isArray(fieldValue)) {
490
+ fieldValue.forEach((item, index)=>collectSingle(item, `${fieldPath}.${index}`));
491
+ } else {
492
+ collectSingle(fieldValue, fieldPath);
493
+ }
494
+ continue;
495
+ }
496
+ if (field.type === "group" && isRecord(fieldValue) && field.fields?.length) {
497
+ targets.push(...collectUploadTargets({
498
+ data: fieldValue,
499
+ fields: field.fields,
500
+ path: fieldPath
501
+ }));
502
+ continue;
503
+ }
504
+ if (field.type === "array" && Array.isArray(fieldValue) && field.fields?.length) {
505
+ fieldValue.forEach((item, index)=>{
506
+ if (!isRecord(item)) return;
507
+ targets.push(...collectUploadTargets({
508
+ data: item,
509
+ fields: field.fields,
510
+ path: `${fieldPath}.${index}`
511
+ }));
512
+ });
513
+ continue;
514
+ }
515
+ if (field.type === "blocks" && Array.isArray(fieldValue) && field.blocks?.length) {
516
+ const blocksBySlug = new Map(field.blocks.map((block)=>[
517
+ block.slug,
518
+ block
519
+ ]));
520
+ fieldValue.forEach((item, index)=>{
521
+ if (!isRecord(item)) return;
522
+ const blockType = typeof item.blockType === "string" ? item.blockType : typeof item.type === "string" ? item.type : typeof item.slug === "string" ? item.slug : null;
523
+ if (!blockType) return;
524
+ const blockConfig = blocksBySlug.get(blockType);
525
+ if (!blockConfig?.fields?.length) return;
526
+ targets.push(...collectUploadTargets({
527
+ data: item,
528
+ fields: blockConfig.fields,
529
+ path: `${fieldPath}.${index}`
530
+ }));
531
+ });
532
+ }
533
+ }
534
+ return targets;
535
+ };
429
536
  const validateRelationshipTargetsExist = async ({ data, fields, req })=>{
430
537
  const targets = collectRelationshipTargets({
431
538
  data,
432
539
  fields
433
540
  });
434
- const invalidTargets = [];
435
- for (const target of targets){
541
+ const targetResults = await Promise.all(targets.map(async (target)=>{
436
542
  try {
437
543
  await req.payload.findByID({
438
544
  collection: target.collection,
@@ -441,11 +547,19 @@ const validateRelationshipTargetsExist = async ({ data, fields, req })=>{
441
547
  overrideAccess: false,
442
548
  req
443
549
  });
550
+ return null;
444
551
  } catch {
445
- invalidTargets.push(target);
552
+ return target;
446
553
  }
447
- }
448
- return invalidTargets;
554
+ }));
555
+ return targetResults.filter((target)=>Boolean(target));
556
+ };
557
+ const getUploadTargetsOutsideAttachments = ({ allowedAttachmentKeys, data, fields })=>{
558
+ if (allowedAttachmentKeys.size === 0) return [];
559
+ return collectUploadTargets({
560
+ data,
561
+ fields
562
+ }).filter((target)=>!allowedAttachmentKeys.has(`${target.collection}:${String(target.id)}`));
449
563
  };
450
564
  const getMentionSummary = (mentions)=>mentions?.map((mention)=>({
451
565
  collection: "collection" in mention ? mention.collection : undefined,
@@ -453,6 +567,39 @@ const getMentionSummary = (mentions)=>mentions?.map((mention)=>({
453
567
  slug: mention.slug,
454
568
  type: mention.type
455
569
  })) || [];
570
+ const getMediaAttachmentContext = async ({ allowedCollectionsBySlug, attachments, collections, req })=>{
571
+ if (!attachments?.length) return [];
572
+ const contexts = [];
573
+ const seen = new Set();
574
+ for (const attachment of attachments.slice(0, 8)){
575
+ if (attachment.type !== "media" || !attachment.collection || !attachment.id) continue;
576
+ const key = `${attachment.collection}:${attachment.id}`;
577
+ if (seen.has(key)) continue;
578
+ const collectionConfig = allowedCollectionsBySlug.get(attachment.collection);
579
+ if (!collectionConfig?.upload) continue;
580
+ const doc = await req.payload.findByID({
581
+ collection: attachment.collection,
582
+ depth: 1,
583
+ id: attachment.id,
584
+ overrideAccess: false,
585
+ req
586
+ }).catch(()=>null);
587
+ if (!doc) continue;
588
+ seen.add(key);
589
+ contexts.push({
590
+ attachment,
591
+ collection: attachment.collection,
592
+ doc,
593
+ schema: describeCollectionLikeConfig({
594
+ config: collectionConfig,
595
+ permissions: collections,
596
+ type: "collection"
597
+ }),
598
+ type: "mediaAttachment"
599
+ });
600
+ }
601
+ return contexts;
602
+ };
456
603
  const formatProposalIssuesForRetry = (issues)=>{
457
604
  return issues.slice(0, 6).map((issue)=>{
458
605
  switch(issue.code){
@@ -500,9 +647,19 @@ const createCollectionAliasMap = (collections)=>{
500
647
  };
501
648
  const getLikelyCollectionMatches = ({ aliasMap, prompt })=>{
502
649
  const normalizedPrompt = prompt.toLowerCase();
503
- const matches = Object.entries(aliasMap).filter(([alias])=>normalizedPrompt.includes(alias)).map(([, slug])=>slug);
650
+ const matches = new Set();
651
+ const aliases = Object.keys(aliasMap).sort((a, b)=>b.length - a.length);
652
+ const aliasPattern = aliases.length > 0 ? new RegExp(aliases.map((alias)=>alias.replace(regexSpecialCharactersPattern, "\\$&")).join("|"), "g") : null;
653
+ if (!aliasPattern) return [];
654
+ for (const match of normalizedPrompt.matchAll(aliasPattern)){
655
+ const alias = match[0];
656
+ const slug = aliasMap[alias];
657
+ if (slug) {
658
+ matches.add(slug);
659
+ }
660
+ }
504
661
  return [
505
- ...new Set(matches)
662
+ ...matches
506
663
  ];
507
664
  };
508
665
  const hasWriteIntent = (prompt)=>{
@@ -517,15 +674,17 @@ const getIntentToolChoice = (prompt)=>{
517
674
  type: "tool"
518
675
  };
519
676
  }
520
- if (/\b(create|build|generate|write|draft|make|add)\b/.test(normalizedPrompt)) {
677
+ // Update existing document (add block, modify content, etc.)
678
+ if (/\b(update|edit|change|modify|einfügen|hinzufügen|addieren)\b/.test(normalizedPrompt)) {
521
679
  return {
522
- toolName: "proposeCreateDoc",
680
+ toolName: "proposeUpdateDoc",
523
681
  type: "tool"
524
682
  };
525
683
  }
526
- if (/\b(update|edit|change|revise|refine|rewrite|translate)\b/.test(normalizedPrompt)) {
684
+ // Create a new document
685
+ if (/\b(create|build|generate|write|draft|make|add|füge|einfügen|hinzufügen|addieren)\b/.test(normalizedPrompt)) {
527
686
  return {
528
- toolName: "proposeUpdateDoc",
687
+ toolName: "proposeCreateDoc",
529
688
  type: "tool"
530
689
  };
531
690
  }
@@ -544,7 +703,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
544
703
  }, {
545
704
  status: 400
546
705
  });
547
- const selectedLocales = (body?.mentions?.filter((mention)=>mention.type === "locale" && mention.slug).map((mention)=>mention.slug) || []).filter((locale, index, array)=>array.indexOf(locale) === index);
706
+ const selectedLocales = [];
707
+ const selectedLocaleSet = new Set();
708
+ for (const mention of body?.mentions || []){
709
+ if (mention.type !== "locale" || !mention.slug || selectedLocaleSet.has(mention.slug)) continue;
710
+ selectedLocaleSet.add(mention.slug);
711
+ selectedLocales.push(mention.slug);
712
+ }
548
713
  const activeLocale = selectedLocales.at(-1);
549
714
  const mentionSummary = getMentionSummary(body?.mentions);
550
715
  if (e2eModeEnabled()) {
@@ -655,8 +820,18 @@ export const createChatHandler = (options = {})=>async (req)=>{
655
820
  return signedProposal;
656
821
  };
657
822
  const collectionSlugs = getAllowedCollectionSlugs(req, options.collections);
658
- const globalSlugs = req.payload.config.globals?.map((global)=>global.slug) || [];
659
- const allowedCollections = req.payload.config.collections.filter((collection)=>collectionSlugs.includes(collection.slug));
823
+ const collectionSlugSet = new Set(collectionSlugs);
824
+ const globalConfigs = req.payload.config.globals || [];
825
+ const globalSlugs = globalConfigs.map((global)=>global.slug);
826
+ const globalConfigsBySlug = new Map(globalConfigs.map((global)=>[
827
+ global.slug,
828
+ global
829
+ ]));
830
+ const allowedCollections = req.payload.config.collections.filter((collection)=>collectionSlugSet.has(collection.slug));
831
+ const allowedCollectionsBySlug = new Map(allowedCollections.map((collection)=>[
832
+ collection.slug,
833
+ collection
834
+ ]));
660
835
  if (collectionSlugs.length === 0) {
661
836
  logHandlerEvent(req, "warn", {
662
837
  debug,
@@ -673,10 +848,10 @@ export const createChatHandler = (options = {})=>async (req)=>{
673
848
  fields: collection.fields,
674
849
  parent: collection.slug
675
850
  })),
676
- ...req.payload.config.globals?.flatMap((global)=>collectBlocks({
851
+ ...globalConfigs.flatMap((global)=>collectBlocks({
677
852
  fields: global.fields,
678
853
  parent: global.slug
679
- })) || []
854
+ }))
680
855
  ];
681
856
  const mentionContext = await getMentionContext({
682
857
  blockContexts,
@@ -686,15 +861,27 @@ export const createChatHandler = (options = {})=>async (req)=>{
686
861
  mentions: body?.mentions,
687
862
  req
688
863
  });
689
- const mentionedCollectionSlugs = (body?.mentions?.flatMap((mention)=>{
690
- if (mention.type === "collection" && mention.slug) return [
691
- mention.slug
692
- ];
693
- if (mention.type === "doc" && mention.collection) return [
694
- mention.collection
695
- ];
696
- return [];
697
- }).filter((slug)=>collectionSlugs.includes(slug)) || []).filter((slug, index, array)=>array.indexOf(slug) === index);
864
+ const mediaAttachmentContext = await getMediaAttachmentContext({
865
+ allowedCollectionsBySlug,
866
+ attachments: body?.attachments,
867
+ collections: options.collections,
868
+ req
869
+ });
870
+ const allowedAttachmentKeys = new Set(mediaAttachmentContext.flatMap((context)=>{
871
+ const attachment = context.attachment;
872
+ return isRecord(attachment) && typeof attachment.collection === "string" && typeof attachment.id === "string" ? [
873
+ `${attachment.collection}:${attachment.id}`
874
+ ] : [];
875
+ }));
876
+ mentionContext.push(...mediaAttachmentContext);
877
+ const mentionedCollectionSlugs = [];
878
+ const mentionedCollectionSlugSet = new Set();
879
+ for (const mention of body?.mentions || []){
880
+ const slug = mention.type === "collection" && mention.slug ? mention.slug : mention.type === "doc" && mention.collection ? mention.collection : null;
881
+ if (!slug || !collectionSlugSet.has(slug) || mentionedCollectionSlugSet.has(slug)) continue;
882
+ mentionedCollectionSlugSet.add(slug);
883
+ mentionedCollectionSlugs.push(slug);
884
+ }
698
885
  const createRequiredFieldsByCollection = Object.fromEntries(allowedCollections.map((collection)=>[
699
886
  collection.slug,
700
887
  getRequiredFieldInfos(collection.fields, collection.admin?.useAsTitle)
@@ -722,7 +909,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
722
909
  });
723
910
  const writeIntent = hasWriteIntent(prompt);
724
911
  const inferredCollectionSlug = mentionedCollectionSlugs.length === 1 ? mentionedCollectionSlugs[0] : mentionedCollectionSlugs.length === 0 && likelyCollectionMatches.length === 1 ? likelyCollectionMatches[0] : undefined;
725
- const inferredCollectionConfig = inferredCollectionSlug ? allowedCollections.find((collection)=>collection.slug === inferredCollectionSlug) : undefined;
912
+ const inferredCollectionConfig = inferredCollectionSlug ? allowedCollectionsBySlug.get(inferredCollectionSlug) : undefined;
726
913
  if (inferredCollectionConfig && !mentionContext.some((item)=>item.type === "collection" && item.slug === inferredCollectionConfig.slug)) {
727
914
  mentionContext.push({
728
915
  ...describeCollectionLikeConfig({
@@ -763,7 +950,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
763
950
  };
764
951
  const tools = {
765
952
  getDoc: {
766
- description: "Read one document by collection slug and document id.",
953
+ description: "Read a document by collection and id.",
767
954
  inputSchema: z.object({
768
955
  collection: collectionSlugSchema,
769
956
  id: z.string().min(1)
@@ -782,13 +969,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
782
969
  }
783
970
  },
784
971
  listCollections: {
785
- description: "List AI-enabled Payload collections. Omit slug for compact summaries; pass slug to get full field schema for one collection.",
972
+ description: "List collections; pass slug for one full schema.",
786
973
  inputSchema: z.object({
787
974
  slug: collectionSlugSchema.optional()
788
975
  }),
789
976
  execute: async ({ slug })=>{
790
977
  if (slug) {
791
- const collection = allowedCollections.find((item)=>item.slug === slug);
978
+ const collection = allowedCollectionsBySlug.get(slug);
792
979
  if (!collection) {
793
980
  return createToolError({
794
981
  message: `Unknown collection: ${slug}`,
@@ -810,12 +997,12 @@ export const createChatHandler = (options = {})=>async (req)=>{
810
997
  }
811
998
  },
812
999
  getGlobal: {
813
- description: "Read one Payload CMS global by slug.",
1000
+ description: "Read a global by slug.",
814
1001
  inputSchema: z.object({
815
1002
  slug: z.string().min(1)
816
1003
  }),
817
1004
  execute: async ({ slug })=>{
818
- const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
1005
+ const globalConfig = globalConfigsBySlug.get(slug);
819
1006
  if (!globalConfig) {
820
1007
  return createToolError({
821
1008
  message: `Unknown global: ${slug}`,
@@ -835,14 +1022,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
835
1022
  }
836
1023
  },
837
1024
  listGlobals: {
838
- description: "List Payload globals. Omit slug for compact summaries; pass slug to get full field schema for one global.",
1025
+ description: "List globals; pass slug for one full schema.",
839
1026
  inputSchema: z.object({
840
1027
  slug: z.string().optional()
841
1028
  }),
842
1029
  execute: async ({ slug })=>{
843
- const globals = req.payload.config.globals || [];
844
1030
  if (slug) {
845
- const global = globals.find((item)=>item.slug === slug);
1031
+ const global = globalConfigsBySlug.get(slug);
846
1032
  if (!global) {
847
1033
  return createToolError({
848
1034
  message: `Unknown global: ${slug}`,
@@ -855,14 +1041,14 @@ export const createChatHandler = (options = {})=>async (req)=>{
855
1041
  type: "global"
856
1042
  });
857
1043
  }
858
- return globals.map((global)=>describeCollectionLikeSummary({
1044
+ return globalConfigs.map((global)=>describeCollectionLikeSummary({
859
1045
  config: global,
860
1046
  type: "global"
861
1047
  }));
862
1048
  }
863
1049
  },
864
1050
  proposeCreateDoc: {
865
- description: "Prepare a CMS document creation proposal. This does not write to the database. Use exact field names from listCollections. Include every required field for the target collection. For localizedData, include every localized required field in every locale entry, and include non-localized required fields in the first locale entry. For array fields, provide arrays of objects matching their child fields. For richText fields, prefer plain text or omit if unsure. Use localizedData when writing multiple locales in one proposal.",
1051
+ description: "Propose document creation. Use exact schema fields; include required fields. Use localizedData for multi-locale writes.",
866
1052
  inputSchema: z.object({
867
1053
  collection: collectionSlugSchema,
868
1054
  data: z.record(z.string(), z.unknown()).optional(),
@@ -874,7 +1060,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
874
1060
  execute: async ({ collection, data, label, localizedData })=>{
875
1061
  const permissionError = getDisallowedCollectionActionError(collection, "create");
876
1062
  if (permissionError) return permissionError;
877
- const collectionConfig = allowedCollections.find((item)=>item.slug === collection);
1063
+ const collectionConfig = allowedCollectionsBySlug.get(collection);
878
1064
  const collectionFields = collectionConfig?.fields || [];
879
1065
  const preparedData = prepareProposalWriteData({
880
1066
  collectionConfig: collectionConfig,
@@ -935,6 +1121,31 @@ export const createChatHandler = (options = {})=>async (req)=>{
935
1121
  });
936
1122
  }
937
1123
  }
1124
+ const uploadTargetsOutsideAttachments = [
1125
+ ...preparedData.data ? getUploadTargetsOutsideAttachments({
1126
+ allowedAttachmentKeys,
1127
+ data: preparedData.data,
1128
+ fields: collectionFields
1129
+ }) : [],
1130
+ ...preparedData.localizedData ? Object.values(preparedData.localizedData).flatMap((localeData)=>getUploadTargetsOutsideAttachments({
1131
+ allowedAttachmentKeys,
1132
+ data: localeData,
1133
+ fields: collectionFields
1134
+ })) : []
1135
+ ];
1136
+ if (uploadTargetsOutsideAttachments.length > 0) {
1137
+ return createToolError({
1138
+ collection,
1139
+ details: {
1140
+ allowedAttachments: [
1141
+ ...allowedAttachmentKeys
1142
+ ],
1143
+ uploadTargetsOutsideAttachments
1144
+ },
1145
+ message: `Create proposal for ${collection} uses upload references that are not in the uploaded attachments: ${uploadTargetsOutsideAttachments.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}. Use only uploaded media attachment IDs for upload fields.`,
1146
+ tool: "proposeCreateDoc"
1147
+ });
1148
+ }
938
1149
  const invalidRelationshipTargets = [
939
1150
  ...preparedData.data ? await validateRelationshipTargetsExist({
940
1151
  data: preparedData.data,
@@ -978,7 +1189,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
978
1189
  }
979
1190
  },
980
1191
  proposeDeleteDoc: {
981
- description: "Prepare a CMS document deletion proposal. This does not write to the database.",
1192
+ description: "Propose document deletion.",
982
1193
  inputSchema: z.object({
983
1194
  collection: collectionSlugSchema,
984
1195
  id: z.string().min(1),
@@ -1000,7 +1211,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
1000
1211
  }
1001
1212
  },
1002
1213
  proposeUpdateDoc: {
1003
- description: "Prepare a CMS document update proposal. This does not write to the database. Use exact field names from listCollections. For array fields, provide arrays of objects matching their child fields. For richText fields, prefer plain text or omit if unsure. Use localizedData when writing multiple locales in one proposal.",
1214
+ description: "Propose document update. Use exact schema fields. Use localizedData for multi-locale writes.",
1004
1215
  inputSchema: z.object({
1005
1216
  collection: collectionSlugSchema,
1006
1217
  data: z.record(z.string(), z.unknown()).optional(),
@@ -1013,7 +1224,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
1013
1224
  execute: async ({ collection, data, id, label, localizedData })=>{
1014
1225
  const permissionError = getDisallowedCollectionActionError(collection, "update");
1015
1226
  if (permissionError) return permissionError;
1016
- const collectionConfig = allowedCollections.find((item)=>item.slug === collection);
1227
+ const collectionConfig = allowedCollectionsBySlug.get(collection);
1017
1228
  const collectionFields = collectionConfig?.fields || [];
1018
1229
  const preparedData = prepareProposalWriteData({
1019
1230
  collectionConfig: collectionConfig,
@@ -1055,6 +1266,31 @@ export const createChatHandler = (options = {})=>async (req)=>{
1055
1266
  tool: "proposeUpdateDoc"
1056
1267
  });
1057
1268
  }
1269
+ const uploadTargetsOutsideAttachments = [
1270
+ ...preparedData.data ? getUploadTargetsOutsideAttachments({
1271
+ allowedAttachmentKeys,
1272
+ data: preparedData.data,
1273
+ fields: collectionFields
1274
+ }) : [],
1275
+ ...preparedData.localizedData ? Object.values(preparedData.localizedData).flatMap((localeData)=>getUploadTargetsOutsideAttachments({
1276
+ allowedAttachmentKeys,
1277
+ data: localeData,
1278
+ fields: collectionFields
1279
+ })) : []
1280
+ ];
1281
+ if (uploadTargetsOutsideAttachments.length > 0) {
1282
+ return createToolError({
1283
+ collection,
1284
+ details: {
1285
+ allowedAttachments: [
1286
+ ...allowedAttachmentKeys
1287
+ ],
1288
+ uploadTargetsOutsideAttachments
1289
+ },
1290
+ message: `Update proposal for ${collection} uses upload references that are not in the uploaded attachments: ${uploadTargetsOutsideAttachments.map((target)=>`${target.path} -> ${target.collection}:${target.id}`).join(", ")}. Use only uploaded media attachment IDs for upload fields.`,
1291
+ tool: "proposeUpdateDoc"
1292
+ });
1293
+ }
1058
1294
  const proposal = {
1059
1295
  action: "update",
1060
1296
  collection,
@@ -1073,7 +1309,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
1073
1309
  }
1074
1310
  },
1075
1311
  proposeUpdateGlobal: {
1076
- description: "Prepare a Payload global update proposal. This does not write to the database. Use localizedData when writing multiple locales in one proposal.",
1312
+ description: "Propose global update. Use localizedData for multi-locale writes.",
1077
1313
  inputSchema: z.object({
1078
1314
  data: z.record(z.string(), z.unknown()).optional(),
1079
1315
  label: z.string().min(1),
@@ -1083,7 +1319,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
1083
1319
  message: "Either data or localizedData is required."
1084
1320
  }),
1085
1321
  execute: async ({ data, label, localizedData, slug })=>{
1086
- const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
1322
+ const globalConfig = globalConfigsBySlug.get(slug);
1087
1323
  if (!globalConfig) {
1088
1324
  return createToolError({
1089
1325
  message: `Unknown global: ${slug}`,
@@ -1129,19 +1365,24 @@ export const createChatHandler = (options = {})=>async (req)=>{
1129
1365
  }
1130
1366
  },
1131
1367
  searchDocs: {
1132
- description: "Search documents in one collection. Use query for a loose text search where possible.",
1368
+ description: "Search documents in one collection.",
1133
1369
  inputSchema: z.object({
1134
1370
  collection: collectionSlugSchema,
1135
1371
  limit: z.number().int().min(1).max(10).default(5),
1136
1372
  query: z.string().optional()
1137
1373
  }),
1138
1374
  execute: async ({ collection, limit, query })=>{
1139
- const collectionConfig = allowedCollections.find((item)=>item.slug === collection);
1140
- const searchableFields = collectionConfig?.fields.filter((field)=>"name" in field && [
1375
+ const collectionConfig = allowedCollectionsBySlug.get(collection);
1376
+ const searchableFields = collectionConfig?.fields.flatMap((field)=>{
1377
+ if (!("name" in field) || ![
1141
1378
  "email",
1142
1379
  "text",
1143
1380
  "textarea"
1144
- ].includes(field.type)).map((field)=>"name" in field ? field.name : null).filter(Boolean) || [];
1381
+ ].includes(field.type) || !field.name) return [];
1382
+ return [
1383
+ field.name
1384
+ ];
1385
+ }) || [];
1145
1386
  const where = query && searchableFields.length > 0 ? {
1146
1387
  or: searchableFields.map((field)=>({
1147
1388
  [field]: {
@@ -1184,6 +1425,9 @@ export const createChatHandler = (options = {})=>async (req)=>{
1184
1425
  "You are a Payload CMS assistant. Inspect schema/content with tools before proposing writes.",
1185
1426
  "Mentions define the active CMS scope. Locale mentions define active locale; multiple locales require localizedData keyed by locale.",
1186
1427
  "Writes are proposals only. Never claim changes were applied before user confirmation.",
1428
+ "Uploaded media attachments appear in context as mediaAttachment entries. Use their exact IDs for upload fields.",
1429
+ "If an uploaded media document has editable descriptive fields, propose an update to that media document with suitable values.",
1430
+ "If a collection has a required `slug` field and the user does not provide it, generate a URL‑friendly slug from the title or from the first meaningful word of the prompt.",
1187
1431
  "For create/update/delete requests, you must use proposal tools. Do not end with plain text if the user asked for a content change.",
1188
1432
  "If the user asks to create, update, refine, translate, remove, or delete content, produce at least one proposal tool call unless blocked by missing schema information or permissions.",
1189
1433
  "Put concrete field values only in tool data, not visible text.",
@@ -0,0 +1,7 @@
1
+ import { type PayloadHandler } from "payload";
2
+ export type MediaUploadOptions = {
3
+ acceptedMimeTypes?: string[];
4
+ collectionSlug: string;
5
+ maxFileSize?: number;
6
+ };
7
+ export declare const createMediaUploadHandler: ({ acceptedMimeTypes, collectionSlug, maxFileSize }: MediaUploadOptions) => PayloadHandler;