@mvriu5/payload-ai 0.5.8 → 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.
- package/README.md +62 -6
- package/dist/ai/{proposals.d.ts → proposalSigning.d.ts} +1 -1
- package/dist/ai/{proposals.js → proposalSigning.js} +6 -11
- package/dist/ai/providerOptions.d.ts +17 -1
- package/dist/ai/providerOptions.js +35 -7
- package/dist/ai/providerRuntime.d.ts +2 -2
- package/dist/ai/providerRuntime.js +50 -16
- package/dist/ai/sensitiveData.js +2 -6
- package/dist/components/AIInput.d.ts +2 -1
- package/dist/components/AIInput.js +327 -41
- package/dist/components/AIInput.module.css +41 -6
- package/dist/components/APIKeyField.d.ts +3 -0
- package/dist/components/{AIApiKeyField.js → APIKeyField.js} +3 -4
- package/dist/components/ActionToast.d.ts +30 -0
- package/dist/components/{AIActionProposalList.js → ActionToast.js} +78 -8
- package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +49 -5
- package/dist/components/AuditLogList.d.ts +32 -0
- package/dist/components/AuditLogList.js +67 -0
- package/dist/components/AuditLogList.module.css +152 -0
- package/dist/components/DiffDialog.d.ts +19 -0
- package/dist/components/DiffDialog.js +396 -0
- package/dist/components/DiffDialog.module.css +333 -0
- package/dist/components/Icons.d.ts +9 -0
- package/dist/components/Icons.js +193 -0
- package/dist/components/MentionPopover.d.ts +19 -0
- package/dist/components/{CollectionMentionPopover.js → MentionPopover.js} +11 -4
- package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
- package/dist/components/hooks/useAISettings.d.ts +6 -4
- package/dist/components/hooks/useAISettings.js +18 -3
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
- package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
- package/dist/exports/client.d.ts +4 -2
- package/dist/exports/client.js +4 -2
- package/dist/handlers/applyActionHandler.d.ts +8 -0
- package/dist/handlers/applyActionHandler.js +563 -0
- package/dist/handlers/auditLogHandler.d.ts +6 -0
- package/dist/handlers/auditLogHandler.js +39 -0
- package/dist/handlers/chatHandler.d.ts +42 -0
- package/dist/handlers/chatHandler.js +634 -0
- package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
- package/dist/handlers/mentionSuggestionHandler.js +97 -0
- package/dist/handlers/proposalDiffHandler.d.ts +7 -0
- package/dist/handlers/proposalDiffHandler.js +208 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +151 -8
- package/dist/payload/collectionPermissions.d.ts +14 -13
- package/dist/payload/collectionPermissions.js +3 -2
- package/dist/payload/normalizeData.d.ts +8 -0
- package/dist/payload/normalizeData.js +25 -3
- package/dist/payload/schemaContext.d.ts +79 -9
- package/dist/payload/schemaContext.js +231 -22
- package/dist/payload/shared.d.ts +33 -0
- package/dist/payload/shared.js +114 -0
- package/package.json +7 -2
- package/dist/components/AIActionProposalList.d.ts +0 -25
- package/dist/components/AIApiKeyField.d.ts +0 -2
- package/dist/components/CollectionMentionPopover.d.ts +0 -16
- package/dist/components/hooks/utils.d.ts +0 -1
- package/dist/components/hooks/utils.js +0 -3
- package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -7
- package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -169
- package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
- package/dist/endpoints/aiChatEndpointHandler.js +0 -321
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +0 -63
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
|
|
2
|
+
import { getDocLabel } from "../payload/shared.js";
|
|
3
|
+
const getLocaleCodes = (req)=>{
|
|
4
|
+
const localization = req.payload.config.localization;
|
|
5
|
+
if (!localization) return [];
|
|
6
|
+
if (Array.isArray(localization.localeCodes)) return localization.localeCodes;
|
|
7
|
+
const locales = localization.locales || [];
|
|
8
|
+
return locales.flatMap((locale)=>{
|
|
9
|
+
if (typeof locale === "string") return [
|
|
10
|
+
locale
|
|
11
|
+
];
|
|
12
|
+
if (locale && typeof locale === "object" && "code" in locale && typeof locale.code === "string") return [
|
|
13
|
+
locale.code
|
|
14
|
+
];
|
|
15
|
+
return [];
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
const hasLocalizedFields = (fields)=>{
|
|
19
|
+
return fields.some((field)=>{
|
|
20
|
+
return Boolean(field.localized) || Boolean(field.fields?.length && hasLocalizedFields(field.fields)) || Boolean(field.tabs?.some((tab)=>hasLocalizedFields(tab.fields || [])));
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
export const createMentionSuggestionHandler = (options = {})=>async (req)=>{
|
|
24
|
+
if (!req.user) return Response.json({
|
|
25
|
+
error: "Unauthorized"
|
|
26
|
+
}, {
|
|
27
|
+
status: 401
|
|
28
|
+
});
|
|
29
|
+
const body = req.json ? await req.json().catch(()=>null) : null;
|
|
30
|
+
const query = body?.query?.trim();
|
|
31
|
+
const collectionSlug = body?.collectionSlug?.trim();
|
|
32
|
+
if (!query && !collectionSlug) return Response.json({
|
|
33
|
+
suggestions: []
|
|
34
|
+
});
|
|
35
|
+
const suggestions = [];
|
|
36
|
+
const suggestionKeys = new Set();
|
|
37
|
+
const normalizedQuery = query?.toLowerCase();
|
|
38
|
+
const collections = req.payload.config.collections.filter((collection)=>{
|
|
39
|
+
if (!isCollectionActionAllowed({
|
|
40
|
+
action: "read",
|
|
41
|
+
permissions: options.collections,
|
|
42
|
+
req,
|
|
43
|
+
slug: collection.slug
|
|
44
|
+
})) return false;
|
|
45
|
+
if (collectionSlug) return collection.slug === collectionSlug;
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
const addSuggestion = ({ collectionSlug, doc, requireLabelMatch = false, useAsTitle })=>{
|
|
49
|
+
const id = doc.id?.toString();
|
|
50
|
+
if (!id) return;
|
|
51
|
+
const key = `${collectionSlug}:${id}`;
|
|
52
|
+
if (suggestionKeys.has(key)) return;
|
|
53
|
+
const label = getDocLabel(doc, useAsTitle);
|
|
54
|
+
if (requireLabelMatch && normalizedQuery && !label.toLowerCase().includes(normalizedQuery)) return;
|
|
55
|
+
suggestionKeys.add(key);
|
|
56
|
+
suggestions.push({
|
|
57
|
+
collection: collectionSlug,
|
|
58
|
+
id,
|
|
59
|
+
label,
|
|
60
|
+
slug: key,
|
|
61
|
+
type: "doc"
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
for (const collection of collections){
|
|
65
|
+
if (suggestions.length >= 5) break;
|
|
66
|
+
const collectionFields = collection.fields;
|
|
67
|
+
const localeCodes = query && hasLocalizedFields(collectionFields) ? getLocaleCodes(req) : [];
|
|
68
|
+
const localesToSearch = localeCodes.length > 0 ? localeCodes : [
|
|
69
|
+
null
|
|
70
|
+
];
|
|
71
|
+
for (const locale of localesToSearch){
|
|
72
|
+
if (suggestions.length >= 5) break;
|
|
73
|
+
const result = await req.payload.find({
|
|
74
|
+
collection: collection.slug,
|
|
75
|
+
depth: 0,
|
|
76
|
+
limit: query ? 100 : 10,
|
|
77
|
+
...locale ? {
|
|
78
|
+
locale
|
|
79
|
+
} : {},
|
|
80
|
+
overrideAccess: false,
|
|
81
|
+
req
|
|
82
|
+
});
|
|
83
|
+
for (const doc of result.docs){
|
|
84
|
+
if (suggestions.length >= 5) break;
|
|
85
|
+
addSuggestion({
|
|
86
|
+
collectionSlug: collection.slug,
|
|
87
|
+
doc,
|
|
88
|
+
requireLabelMatch: Boolean(normalizedQuery),
|
|
89
|
+
useAsTitle: collection.admin?.useAsTitle
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return Response.json({
|
|
95
|
+
suggestions: suggestions.slice(0, 5)
|
|
96
|
+
});
|
|
97
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PayloadHandler } from "payload";
|
|
2
|
+
import { type ResolvedCollectionPermissionMap } from "../payload/collectionPermissions.js";
|
|
3
|
+
type ProposalDiffOptions = {
|
|
4
|
+
collections?: ResolvedCollectionPermissionMap;
|
|
5
|
+
};
|
|
6
|
+
export declare const createProposalDiffHandler: (options?: ProposalDiffOptions) => PayloadHandler;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { verifyActionProposal } from "../ai/proposalSigning.js";
|
|
2
|
+
import { redactSensitiveData } from "../ai/sensitiveData.js";
|
|
3
|
+
import { getCollectionFields, getLocalizedRequiredFallbackData, normalizeAuthData, normalizeDataForFields } from "../payload/normalizeData.js";
|
|
4
|
+
import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
|
|
5
|
+
import { getDefaultLocale, hasLocalizedData, isActionProposal, mergeData } from "../payload/shared.js";
|
|
6
|
+
const applyLocalizedRequiredFallback = ({ data, fallbackSource, fields })=>{
|
|
7
|
+
return mergeData(getLocalizedRequiredFallbackData({
|
|
8
|
+
fields,
|
|
9
|
+
source: fallbackSource,
|
|
10
|
+
target: data
|
|
11
|
+
}), data);
|
|
12
|
+
};
|
|
13
|
+
export const createProposalDiffHandler = (options = {})=>async (req)=>{
|
|
14
|
+
if (!req.user) return Response.json({
|
|
15
|
+
error: "Unauthorized"
|
|
16
|
+
}, {
|
|
17
|
+
status: 401
|
|
18
|
+
});
|
|
19
|
+
const body = req.json ? await req.json().catch(()=>null) : null;
|
|
20
|
+
const proposal = body?.proposal;
|
|
21
|
+
if (!proposal) return Response.json({
|
|
22
|
+
error: "Proposal is required"
|
|
23
|
+
}, {
|
|
24
|
+
status: 400
|
|
25
|
+
});
|
|
26
|
+
if (!verifyActionProposal(proposal)) return Response.json({
|
|
27
|
+
error: "Proposal signature is invalid or expired."
|
|
28
|
+
}, {
|
|
29
|
+
status: 400
|
|
30
|
+
});
|
|
31
|
+
if (!isActionProposal(proposal)) return Response.json({
|
|
32
|
+
error: "Proposal is invalid."
|
|
33
|
+
}, {
|
|
34
|
+
status: 400
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
const defaultLocale = getDefaultLocale(req);
|
|
38
|
+
if (proposal.action === "updateGlobal") {
|
|
39
|
+
const globalConfig = req.payload.config.globals?.find((global)=>global.slug === proposal.slug);
|
|
40
|
+
if (!globalConfig) return Response.json({
|
|
41
|
+
error: "Unknown global"
|
|
42
|
+
}, {
|
|
43
|
+
status: 400
|
|
44
|
+
});
|
|
45
|
+
if (hasLocalizedData(proposal)) {
|
|
46
|
+
const beforeByLocale = {};
|
|
47
|
+
const afterByLocale = {};
|
|
48
|
+
const globalFields = globalConfig.fields || [];
|
|
49
|
+
const defaultLocaleDoc = defaultLocale ? await req.payload.findGlobal({
|
|
50
|
+
depth: 2,
|
|
51
|
+
fallbackLocale: false,
|
|
52
|
+
locale: defaultLocale,
|
|
53
|
+
overrideAccess: false,
|
|
54
|
+
req,
|
|
55
|
+
slug: proposal.slug
|
|
56
|
+
}) : null;
|
|
57
|
+
for (const [locale, localeData] of Object.entries(proposal.localizedData)){
|
|
58
|
+
const normalized = normalizeDataForFields(globalFields, localeData);
|
|
59
|
+
const doc = await req.payload.findGlobal({
|
|
60
|
+
depth: 2,
|
|
61
|
+
fallbackLocale: false,
|
|
62
|
+
locale,
|
|
63
|
+
overrideAccess: false,
|
|
64
|
+
req,
|
|
65
|
+
slug: proposal.slug
|
|
66
|
+
});
|
|
67
|
+
const completedData = applyLocalizedRequiredFallback({
|
|
68
|
+
data: normalized.data,
|
|
69
|
+
fallbackSource: locale === defaultLocale ? doc : defaultLocaleDoc || doc,
|
|
70
|
+
fields: globalFields
|
|
71
|
+
});
|
|
72
|
+
beforeByLocale[locale] = redactSensitiveData(doc);
|
|
73
|
+
afterByLocale[locale] = redactSensitiveData(mergeData(doc, completedData));
|
|
74
|
+
}
|
|
75
|
+
return Response.json({
|
|
76
|
+
after: afterByLocale,
|
|
77
|
+
before: beforeByLocale
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const normalized = normalizeDataForFields(globalConfig.fields || [], proposal.data);
|
|
81
|
+
const doc = await req.payload.findGlobal({
|
|
82
|
+
depth: 2,
|
|
83
|
+
...proposal.locale ? {
|
|
84
|
+
locale: proposal.locale
|
|
85
|
+
} : {},
|
|
86
|
+
overrideAccess: false,
|
|
87
|
+
req,
|
|
88
|
+
slug: proposal.slug
|
|
89
|
+
});
|
|
90
|
+
return Response.json({
|
|
91
|
+
after: redactSensitiveData(mergeData(doc, normalized.data)),
|
|
92
|
+
before: redactSensitiveData(doc)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (!isCollectionActionAllowed({
|
|
96
|
+
action: proposal.action === "delete" ? "delete" : "read",
|
|
97
|
+
permissions: options.collections,
|
|
98
|
+
req,
|
|
99
|
+
slug: proposal.collection
|
|
100
|
+
})) return Response.json({
|
|
101
|
+
error: "Unknown collection"
|
|
102
|
+
}, {
|
|
103
|
+
status: 400
|
|
104
|
+
});
|
|
105
|
+
if (proposal.action === "delete") {
|
|
106
|
+
const doc = await req.payload.findByID({
|
|
107
|
+
collection: proposal.collection,
|
|
108
|
+
depth: 2,
|
|
109
|
+
id: proposal.id,
|
|
110
|
+
...proposal.locale ? {
|
|
111
|
+
locale: proposal.locale
|
|
112
|
+
} : {},
|
|
113
|
+
overrideAccess: false,
|
|
114
|
+
req
|
|
115
|
+
});
|
|
116
|
+
return Response.json({
|
|
117
|
+
after: {},
|
|
118
|
+
before: redactSensitiveData(doc)
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const collectionConfig = req.payload.config.collections.find((collection)=>collection.slug === proposal.collection);
|
|
122
|
+
const collectionFields = getCollectionFields(collectionConfig);
|
|
123
|
+
const normalizeCollectionData = (data)=>normalizeAuthData(collectionConfig, normalizeDataForFields(collectionFields, data));
|
|
124
|
+
if (hasLocalizedData(proposal)) {
|
|
125
|
+
const afterByLocale = {};
|
|
126
|
+
const beforeByLocale = {};
|
|
127
|
+
const defaultLocaleDoc = proposal.action === "update" && defaultLocale ? await req.payload.findByID({
|
|
128
|
+
collection: proposal.collection,
|
|
129
|
+
depth: 2,
|
|
130
|
+
fallbackLocale: false,
|
|
131
|
+
id: proposal.id,
|
|
132
|
+
locale: defaultLocale,
|
|
133
|
+
overrideAccess: false,
|
|
134
|
+
req
|
|
135
|
+
}) : null;
|
|
136
|
+
let createFallbackSource = null;
|
|
137
|
+
for (const [locale, localeData] of Object.entries(proposal.localizedData)){
|
|
138
|
+
const normalized = normalizeCollectionData(localeData);
|
|
139
|
+
const fallbackSource = proposal.action === "create" ? createFallbackSource || {} : locale === defaultLocale ? await req.payload.findByID({
|
|
140
|
+
collection: proposal.collection,
|
|
141
|
+
depth: 2,
|
|
142
|
+
fallbackLocale: false,
|
|
143
|
+
id: proposal.id,
|
|
144
|
+
locale,
|
|
145
|
+
overrideAccess: false,
|
|
146
|
+
req
|
|
147
|
+
}) : defaultLocaleDoc || {};
|
|
148
|
+
const completedData = applyLocalizedRequiredFallback({
|
|
149
|
+
data: normalized.data,
|
|
150
|
+
fallbackSource,
|
|
151
|
+
fields: collectionFields
|
|
152
|
+
});
|
|
153
|
+
if (proposal.action === "create") {
|
|
154
|
+
beforeByLocale[locale] = {};
|
|
155
|
+
afterByLocale[locale] = redactSensitiveData(completedData);
|
|
156
|
+
createFallbackSource = mergeData(createFallbackSource || {}, completedData);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const doc = await req.payload.findByID({
|
|
160
|
+
collection: proposal.collection,
|
|
161
|
+
depth: 2,
|
|
162
|
+
fallbackLocale: false,
|
|
163
|
+
id: proposal.id,
|
|
164
|
+
locale,
|
|
165
|
+
overrideAccess: false,
|
|
166
|
+
req
|
|
167
|
+
});
|
|
168
|
+
beforeByLocale[locale] = redactSensitiveData(doc);
|
|
169
|
+
afterByLocale[locale] = redactSensitiveData(mergeData(doc, completedData));
|
|
170
|
+
}
|
|
171
|
+
return Response.json({
|
|
172
|
+
after: afterByLocale,
|
|
173
|
+
before: beforeByLocale
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const normalized = normalizeCollectionData(proposal.data);
|
|
177
|
+
if (proposal.action === "create") {
|
|
178
|
+
return Response.json({
|
|
179
|
+
after: redactSensitiveData(normalized.data),
|
|
180
|
+
before: {}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const doc = await req.payload.findByID({
|
|
184
|
+
collection: proposal.collection,
|
|
185
|
+
depth: 2,
|
|
186
|
+
id: proposal.id,
|
|
187
|
+
...proposal.locale ? {
|
|
188
|
+
locale: proposal.locale
|
|
189
|
+
} : {},
|
|
190
|
+
overrideAccess: false,
|
|
191
|
+
req
|
|
192
|
+
});
|
|
193
|
+
return Response.json({
|
|
194
|
+
after: redactSensitiveData(mergeData(doc, normalized.data)),
|
|
195
|
+
before: redactSensitiveData(doc)
|
|
196
|
+
});
|
|
197
|
+
} catch (err) {
|
|
198
|
+
req.payload.logger.error({
|
|
199
|
+
err,
|
|
200
|
+
msg: "AI proposal diff failed"
|
|
201
|
+
});
|
|
202
|
+
return Response.json({
|
|
203
|
+
error: "Could not load proposal diff."
|
|
204
|
+
}, {
|
|
205
|
+
status: 400
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Config } from "payload";
|
|
2
2
|
import { type AIModelConfig } from "./ai/providerOptions.js";
|
|
3
|
-
import { type
|
|
4
|
-
export type
|
|
3
|
+
import { type CollectionPermissionMap } from "./payload/collectionPermissions.js";
|
|
4
|
+
export type PayloadAIPluginOptions = {
|
|
5
5
|
allowUserApiKeys?: boolean;
|
|
6
|
-
collections?:
|
|
6
|
+
collections?: CollectionPermissionMap;
|
|
7
7
|
disabled?: boolean;
|
|
8
|
+
maxOutputTokens?: number;
|
|
8
9
|
models?: AIModelConfig;
|
|
9
10
|
};
|
|
10
|
-
export
|
|
11
|
-
export declare const payloadAiPlugin: (pluginOptions: PayloadAiPluginOptions) => (config: Config) => Config;
|
|
11
|
+
export declare const payloadAiPlugin: (pluginOptions: PayloadAIPluginOptions) => (config: Config) => Config;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,130 @@
|
|
|
1
1
|
import { aiProviders, getResolvedAIModelConfig } from "./ai/providerOptions.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { createApplyActionHandler } from "./handlers/applyActionHandler.js";
|
|
3
|
+
import { createChatHandler } from "./handlers/chatHandler.js";
|
|
4
|
+
import { createMentionSuggestionHandler } from "./handlers/mentionSuggestionHandler.js";
|
|
5
|
+
import { createProposalDiffHandler } from "./handlers/proposalDiffHandler.js";
|
|
6
|
+
import { createAuditLogHandler } from "./handlers/auditLogHandler.js";
|
|
5
7
|
import { resolveCollectionPermissions } from "./payload/collectionPermissions.js";
|
|
6
8
|
import { isInternalCollection } from "./payload/shared.js";
|
|
9
|
+
const auditLogCollectionSlug = "payload-ai-auditlog";
|
|
10
|
+
const createAIChangesCollection = ()=>({
|
|
11
|
+
slug: auditLogCollectionSlug,
|
|
12
|
+
access: {
|
|
13
|
+
create: ()=>false,
|
|
14
|
+
delete: ()=>false,
|
|
15
|
+
read: ({ req })=>Boolean(req.user),
|
|
16
|
+
update: ()=>false
|
|
17
|
+
},
|
|
18
|
+
admin: {
|
|
19
|
+
defaultColumns: [
|
|
20
|
+
"title",
|
|
21
|
+
"action",
|
|
22
|
+
"additions",
|
|
23
|
+
"removals",
|
|
24
|
+
"createdAt"
|
|
25
|
+
],
|
|
26
|
+
group: "AI",
|
|
27
|
+
useAsTitle: "title"
|
|
28
|
+
},
|
|
29
|
+
labels: {
|
|
30
|
+
plural: "AI Changes",
|
|
31
|
+
singular: "AI Change"
|
|
32
|
+
},
|
|
33
|
+
fields: [
|
|
34
|
+
{
|
|
35
|
+
name: "title",
|
|
36
|
+
type: "text",
|
|
37
|
+
required: true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "action",
|
|
41
|
+
type: "select",
|
|
42
|
+
options: [
|
|
43
|
+
"create",
|
|
44
|
+
"update",
|
|
45
|
+
"delete",
|
|
46
|
+
"updateGlobal"
|
|
47
|
+
],
|
|
48
|
+
required: true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "targetType",
|
|
52
|
+
type: "select",
|
|
53
|
+
options: [
|
|
54
|
+
"collection",
|
|
55
|
+
"global"
|
|
56
|
+
],
|
|
57
|
+
required: true
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "collection",
|
|
61
|
+
type: "text"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "slug",
|
|
65
|
+
type: "text"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "documentID",
|
|
69
|
+
type: "text"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "targetURL",
|
|
73
|
+
type: "text"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "additions",
|
|
77
|
+
type: "number",
|
|
78
|
+
defaultValue: 0
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "removals",
|
|
82
|
+
type: "number",
|
|
83
|
+
defaultValue: 0
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "before",
|
|
87
|
+
type: "json"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "after",
|
|
91
|
+
type: "json"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "proposal",
|
|
95
|
+
type: "json"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "prompt",
|
|
99
|
+
type: "textarea"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "inputTokens",
|
|
103
|
+
type: "number"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "outputTokens",
|
|
107
|
+
type: "number"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "totalTokens",
|
|
111
|
+
type: "number"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "aiResponse",
|
|
115
|
+
type: "textarea"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "userID",
|
|
119
|
+
type: "text"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "userLabel",
|
|
123
|
+
type: "text"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
timestamps: true
|
|
127
|
+
});
|
|
7
128
|
const addAccountFields = ({ allowUserApiKeys, config })=>{
|
|
8
129
|
const adminUserSlug = config.admin?.user;
|
|
9
130
|
if (!adminUserSlug || !config.collections) return;
|
|
@@ -13,6 +134,7 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
|
|
|
13
134
|
name: "aiProvider",
|
|
14
135
|
type: "select",
|
|
15
136
|
defaultValue: "openai",
|
|
137
|
+
label: "AI Provider",
|
|
16
138
|
options: aiProviders
|
|
17
139
|
});
|
|
18
140
|
if (allowUserApiKeys) {
|
|
@@ -24,7 +146,8 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
|
|
|
24
146
|
Field: "payload-ai-plugin/client#AIApiKeyField"
|
|
25
147
|
},
|
|
26
148
|
description: "Optional. If empty, the plugin uses the provider API key from environment variables."
|
|
27
|
-
}
|
|
149
|
+
},
|
|
150
|
+
label: "AI API Key"
|
|
28
151
|
});
|
|
29
152
|
}
|
|
30
153
|
};
|
|
@@ -33,7 +156,11 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
|
33
156
|
const collectionPermissions = resolveCollectionPermissions(pluginOptions.collections);
|
|
34
157
|
const allowUserApiKeys = pluginOptions.allowUserApiKeys !== false;
|
|
35
158
|
const modelConfig = getResolvedAIModelConfig(pluginOptions.models);
|
|
159
|
+
const maxOutputTokens = typeof pluginOptions.maxOutputTokens === "number" && Number.isFinite(pluginOptions.maxOutputTokens) && pluginOptions.maxOutputTokens > 0 ? Math.floor(pluginOptions.maxOutputTokens) : undefined;
|
|
36
160
|
if (!config.collections) config.collections = [];
|
|
161
|
+
if (!config.collections.some((collection)=>collection.slug === auditLogCollectionSlug)) {
|
|
162
|
+
config.collections.push(createAIChangesCollection());
|
|
163
|
+
}
|
|
37
164
|
addAccountFields({
|
|
38
165
|
allowUserApiKeys,
|
|
39
166
|
config
|
|
@@ -55,27 +182,43 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
|
55
182
|
if (!config.admin.components.beforeDashboard) config.admin.components.beforeDashboard = [];
|
|
56
183
|
config.admin.components.beforeDashboard.push(`payload-ai-plugin/client#AIInput`);
|
|
57
184
|
config.endpoints.push({
|
|
58
|
-
handler:
|
|
185
|
+
handler: createChatHandler({
|
|
59
186
|
allowUserApiKeys,
|
|
60
187
|
collections: collectionPermissions,
|
|
188
|
+
maxOutputTokens,
|
|
61
189
|
models: modelConfig
|
|
62
190
|
}),
|
|
63
191
|
method: "post",
|
|
64
192
|
path: "/ai-chat"
|
|
65
193
|
});
|
|
66
194
|
config.endpoints.push({
|
|
67
|
-
handler:
|
|
195
|
+
handler: createApplyActionHandler({
|
|
196
|
+
changeLogCollection: auditLogCollectionSlug,
|
|
68
197
|
collections: collectionPermissions
|
|
69
198
|
}),
|
|
70
199
|
method: "post",
|
|
71
200
|
path: "/ai-apply-action"
|
|
72
201
|
});
|
|
73
202
|
config.endpoints.push({
|
|
74
|
-
handler:
|
|
203
|
+
handler: createAuditLogHandler({
|
|
204
|
+
changeLogCollection: auditLogCollectionSlug
|
|
205
|
+
}),
|
|
206
|
+
method: "get",
|
|
207
|
+
path: "/ai-audit-log"
|
|
208
|
+
});
|
|
209
|
+
config.endpoints.push({
|
|
210
|
+
handler: createProposalDiffHandler({
|
|
211
|
+
collections: collectionPermissions
|
|
212
|
+
}),
|
|
213
|
+
method: "post",
|
|
214
|
+
path: "/ai-proposal-diff"
|
|
215
|
+
});
|
|
216
|
+
config.endpoints.push({
|
|
217
|
+
handler: createMentionSuggestionHandler({
|
|
75
218
|
collections: collectionPermissions
|
|
76
219
|
}),
|
|
77
220
|
method: "post",
|
|
78
|
-
path: "/ai-mention-
|
|
221
|
+
path: "/ai-mention-suggestion"
|
|
79
222
|
});
|
|
80
223
|
if (incomingOnInit) {
|
|
81
224
|
config.onInit = async (payload)=>{
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import type { PayloadHandler } from "payload";
|
|
2
|
-
export type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export type
|
|
6
|
-
export type
|
|
7
|
-
export declare const resolveCollectionPermissions: (collections?:
|
|
2
|
+
export type CollectionAction = "create" | "delete" | "read" | "update";
|
|
3
|
+
type CollectionPermissions = Partial<Record<CollectionAction, boolean>>;
|
|
4
|
+
type CollectionPermissionConfig = true | CollectionPermissions;
|
|
5
|
+
export type CollectionPermissionMap = Partial<Record<string, CollectionPermissionConfig>>;
|
|
6
|
+
export type ResolvedCollectionPermissionMap = Partial<Record<string, Record<CollectionAction, boolean>>>;
|
|
7
|
+
export declare const resolveCollectionPermissions: (collections?: CollectionPermissionMap) => ResolvedCollectionPermissionMap | undefined;
|
|
8
8
|
export declare const getCollectionSlugsForAction: ({ action, permissions, req, }: {
|
|
9
|
-
action:
|
|
10
|
-
permissions?:
|
|
9
|
+
action: CollectionAction;
|
|
10
|
+
permissions?: ResolvedCollectionPermissionMap;
|
|
11
11
|
req: Parameters<PayloadHandler>[0];
|
|
12
12
|
}) => string[];
|
|
13
13
|
export declare const isCollectionActionAllowed: ({ action, permissions, req, slug, }: {
|
|
14
|
-
action:
|
|
15
|
-
permissions?:
|
|
14
|
+
action: CollectionAction;
|
|
15
|
+
permissions?: ResolvedCollectionPermissionMap;
|
|
16
16
|
req: Parameters<PayloadHandler>[0];
|
|
17
17
|
slug: string;
|
|
18
18
|
}) => boolean;
|
|
19
|
-
export declare const getCollectionPermissions: ({ permissions, slug
|
|
20
|
-
permissions?:
|
|
19
|
+
export declare const getCollectionPermissions: ({ permissions, slug }: {
|
|
20
|
+
permissions?: ResolvedCollectionPermissionMap;
|
|
21
21
|
slug: string;
|
|
22
|
-
}) => Record<
|
|
22
|
+
}) => Record<CollectionAction, boolean>;
|
|
23
|
+
export {};
|
|
@@ -10,7 +10,7 @@ const getKnownCollectionSlugs = (req)=>{
|
|
|
10
10
|
};
|
|
11
11
|
export const resolveCollectionPermissions = (collections)=>{
|
|
12
12
|
if (!collections) return undefined;
|
|
13
|
-
|
|
13
|
+
const entries = Object.entries(collections).map(([slug, config])=>{
|
|
14
14
|
if (config === true) {
|
|
15
15
|
return [
|
|
16
16
|
slug,
|
|
@@ -42,7 +42,8 @@ export const resolveCollectionPermissions = (collections)=>{
|
|
|
42
42
|
update: Boolean(config.update)
|
|
43
43
|
}
|
|
44
44
|
];
|
|
45
|
-
})
|
|
45
|
+
});
|
|
46
|
+
return Object.fromEntries(entries);
|
|
46
47
|
};
|
|
47
48
|
export const getCollectionSlugsForAction = ({ action, permissions, req })=>{
|
|
48
49
|
const knownSlugs = getKnownCollectionSlugs(req);
|
|
@@ -2,10 +2,13 @@ export type FieldConfig = {
|
|
|
2
2
|
blocks?: BlockConfig[];
|
|
3
3
|
defaultValue?: unknown;
|
|
4
4
|
fields?: FieldConfig[];
|
|
5
|
+
label?: unknown;
|
|
6
|
+
localized?: boolean;
|
|
5
7
|
name?: string;
|
|
6
8
|
options?: (string | {
|
|
7
9
|
value?: string;
|
|
8
10
|
})[];
|
|
11
|
+
required?: boolean;
|
|
9
12
|
type?: string;
|
|
10
13
|
};
|
|
11
14
|
type BlockConfig = {
|
|
@@ -26,4 +29,9 @@ export declare const isAuthCollection: (collectionConfig?: CollectionConfig | nu
|
|
|
26
29
|
export declare const getCollectionFields: (collectionConfig?: CollectionConfig | null) => FieldConfig[];
|
|
27
30
|
export declare const normalizeAuthData: (collectionConfig: CollectionConfig | undefined, normalized: NormalizedData) => NormalizedData;
|
|
28
31
|
export declare const normalizeDataForFields: (fields: FieldConfig[], data: Record<string, unknown>) => NormalizedData;
|
|
32
|
+
export declare const getLocalizedRequiredFallbackData: ({ fields, source, target, }: {
|
|
33
|
+
fields: FieldConfig[];
|
|
34
|
+
source: Record<string, unknown>;
|
|
35
|
+
target: Record<string, unknown>;
|
|
36
|
+
}) => Record<string, unknown>;
|
|
29
37
|
export {};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
+
import { isRecord } from "./shared.js";
|
|
1
2
|
const SKIP_FIELD = Symbol("skipField");
|
|
2
|
-
const isRecord = (value)=>{
|
|
3
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
4
|
-
};
|
|
5
3
|
const getNamedFields = (fields)=>{
|
|
6
4
|
return fields.filter((field)=>Boolean(field.name));
|
|
7
5
|
};
|
|
@@ -190,3 +188,27 @@ export const normalizeDataForFields = (fields, data)=>{
|
|
|
190
188
|
droppedFields
|
|
191
189
|
};
|
|
192
190
|
};
|
|
191
|
+
export const getLocalizedRequiredFallbackData = ({ fields, source, target })=>{
|
|
192
|
+
const fallbackData = {};
|
|
193
|
+
for (const field of getNamedFields(fields)){
|
|
194
|
+
const targetValue = target[field.name];
|
|
195
|
+
const sourceValue = source[field.name];
|
|
196
|
+
if (field.localized && field.required) {
|
|
197
|
+
if (targetValue === undefined && sourceValue !== undefined) {
|
|
198
|
+
fallbackData[field.name] = sourceValue;
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (field.type === "group" && field.fields?.length && isRecord(sourceValue)) {
|
|
203
|
+
const nestedFallback = getLocalizedRequiredFallbackData({
|
|
204
|
+
fields: field.fields,
|
|
205
|
+
source: sourceValue,
|
|
206
|
+
target: isRecord(targetValue) ? targetValue : {}
|
|
207
|
+
});
|
|
208
|
+
if (Object.keys(nestedFallback).length > 0) {
|
|
209
|
+
fallbackData[field.name] = nestedFallback;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return fallbackData;
|
|
214
|
+
};
|