@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.
- package/README.md +42 -0
- package/dist/components/Icons.d.ts +0 -8
- package/dist/components/Icons.js +4 -216
- package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +1 -2
- package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +25 -34
- package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
- package/dist/components/ai-input/AIInput.js +433 -0
- package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +111 -43
- package/dist/components/ai-input/badge.d.ts +14 -0
- package/dist/components/ai-input/badge.js +85 -0
- package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
- package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +49 -21
- package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
- package/dist/components/dashboard/Dashboard.d.ts +2 -0
- package/dist/components/dashboard/Dashboard.js +15 -0
- package/dist/components/dashboard/Dashboard.module.css +13 -0
- package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
- package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
- package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
- package/dist/components/hooks/useAIChatStream.d.ts +39 -0
- package/dist/components/hooks/useAIChatStream.js +217 -0
- package/dist/components/hooks/useAISettings.js +26 -25
- package/dist/components/hooks/useAuditLog.d.ts +11 -0
- package/dist/components/hooks/useAuditLog.js +41 -0
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
- package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
- package/dist/components/hooks/useMentions.d.ts +58 -0
- package/dist/components/hooks/useMentions.js +276 -0
- package/dist/components/hooks/usePluginConfig.d.ts +52 -0
- package/dist/components/hooks/usePluginConfig.js +20 -0
- package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
- package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
- package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
- package/dist/exports/client.d.ts +2 -2
- package/dist/exports/client.js +2 -2
- package/dist/handlers/applyActionHandler.js +27 -7
- package/dist/handlers/chatHandler.js +302 -58
- package/dist/handlers/mediaUploadHandler.d.ts +7 -0
- package/dist/handlers/mediaUploadHandler.js +99 -0
- package/dist/handlers/mentionSuggestionHandler.js +16 -14
- package/dist/handlers/proposalDiffHandler.js +41 -29
- package/dist/index.d.ts +6 -0
- package/dist/index.js +39 -7
- package/dist/payload/collectionPermissions.js +3 -1
- package/dist/payload/normalizeData.d.ts +0 -6
- package/dist/payload/normalizeData.js +25 -13
- package/dist/payload/proposalData.js +4 -1
- package/dist/payload/schemaContext.js +98 -43
- package/package.json +6 -6
- package/dist/components/AIInput.js +0 -896
- /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
|
|
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
|
|
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.
|
|
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 &&
|
|
319
|
+
if (mention.type === "block" && mention.slug && availableBlockTypeSet.has(mention.slug)) {
|
|
301
320
|
requestedBlockTypes.add(mention.slug);
|
|
302
321
|
}
|
|
303
322
|
}
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
552
|
+
return target;
|
|
446
553
|
}
|
|
447
|
-
}
|
|
448
|
-
return
|
|
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 =
|
|
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
|
-
...
|
|
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
|
-
|
|
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: "
|
|
680
|
+
toolName: "proposeUpdateDoc",
|
|
523
681
|
type: "tool"
|
|
524
682
|
};
|
|
525
683
|
}
|
|
526
|
-
|
|
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: "
|
|
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 =
|
|
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
|
|
659
|
-
const
|
|
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
|
-
...
|
|
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
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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 ?
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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: "
|
|
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 =
|
|
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: "
|
|
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: "
|
|
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 =
|
|
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: "
|
|
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 =
|
|
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.
|
|
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 =
|
|
1140
|
-
const searchableFields = collectionConfig?.fields.
|
|
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)
|
|
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;
|