@mvriu5/payload-ai 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +99 -0
  2. package/dist/ai/proposals.d.ts +12 -0
  3. package/dist/ai/proposals.js +50 -0
  4. package/dist/ai/providerOptions.d.ts +85 -0
  5. package/dist/ai/providerOptions.js +141 -0
  6. package/dist/ai/providerRuntime.d.ts +18 -0
  7. package/dist/ai/providerRuntime.js +53 -0
  8. package/dist/ai/sensitiveData.d.ts +2 -0
  9. package/dist/ai/sensitiveData.js +30 -0
  10. package/dist/components/AIActionProposalList.d.ts +25 -0
  11. package/dist/components/AIActionProposalList.js +94 -0
  12. package/dist/components/AIActionProposalList.module.css +175 -0
  13. package/dist/components/AIApiKeyField.d.ts +2 -0
  14. package/dist/components/AIApiKeyField.js +55 -0
  15. package/dist/components/AIInput.d.ts +1 -0
  16. package/dist/components/AIInput.js +386 -0
  17. package/dist/components/AIInput.module.css +237 -0
  18. package/dist/components/CollectionMentionPopover.d.ts +16 -0
  19. package/dist/components/CollectionMentionPopover.js +77 -0
  20. package/dist/components/CollectionMentionPopover.module.css +67 -0
  21. package/dist/components/hooks/useAISettings.d.ts +10 -0
  22. package/dist/components/hooks/useAISettings.js +56 -0
  23. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +16 -0
  24. package/dist/components/hooks/useDocumentMentionSuggestions.js +53 -0
  25. package/dist/components/hooks/utils.d.ts +1 -0
  26. package/dist/components/hooks/utils.js +3 -0
  27. package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +6 -0
  28. package/dist/endpoints/aiApplyActionEndpointHandler.js +164 -0
  29. package/dist/endpoints/aiChatEndpointHandler.d.ts +31 -0
  30. package/dist/endpoints/aiChatEndpointHandler.js +297 -0
  31. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +6 -0
  32. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +63 -0
  33. package/dist/exports/client.d.ts +2 -0
  34. package/dist/exports/client.js +2 -0
  35. package/dist/index.d.ts +9 -0
  36. package/dist/index.js +75 -0
  37. package/dist/payload/normalizeData.d.ts +29 -0
  38. package/dist/payload/normalizeData.js +192 -0
  39. package/dist/payload/schemaContext.d.ts +52 -0
  40. package/dist/payload/schemaContext.js +162 -0
  41. package/dist/payload/shared.d.ts +2 -0
  42. package/dist/payload/shared.js +11 -0
  43. package/package.json +126 -0
@@ -0,0 +1,164 @@
1
+ import { verifyAIActionProposal } from "../ai/proposals.js";
2
+ import { containsSensitiveData } from "../ai/sensitiveData.js";
3
+ import { getCollectionFields, isAuthCollection, normalizeAuthData, normalizeDataForFields } from "../payload/normalizeData.js";
4
+ const getProposalMeta = (proposal)=>{
5
+ if (!proposal) return {};
6
+ return {
7
+ action: proposal.action,
8
+ collection: "collection" in proposal ? proposal.collection : undefined,
9
+ id: "id" in proposal ? proposal.id : undefined,
10
+ slug: "slug" in proposal ? proposal.slug : undefined
11
+ };
12
+ };
13
+ const getAppliedDocReference = (doc)=>{
14
+ return doc?.id === undefined ? undefined : {
15
+ id: doc.id
16
+ };
17
+ };
18
+ const isKnownCollection = (req, collection)=>{
19
+ return req.payload.config.collections.some((item)=>item.slug === collection);
20
+ };
21
+ const isAllowedCollection = (req, collection, collections)=>{
22
+ if (!collections) return isKnownCollection(req, collection);
23
+ return collections.includes(collection) && isKnownCollection(req, collection);
24
+ };
25
+ const isKnownGlobal = (req, slug)=>{
26
+ return req.payload.config.globals?.some((item)=>item.slug === slug) || false;
27
+ };
28
+ const isRecord = (value)=>{
29
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
30
+ };
31
+ const isActionProposal = (proposal)=>{
32
+ if (!isRecord(proposal) || typeof proposal.label !== "string") return false;
33
+ if (proposal.action === "create") {
34
+ return typeof proposal.collection === "string" && isRecord(proposal.data);
35
+ }
36
+ if (proposal.action === "update") {
37
+ return typeof proposal.collection === "string" && typeof proposal.id === "string" && isRecord(proposal.data);
38
+ }
39
+ if (proposal.action === "delete") {
40
+ return typeof proposal.collection === "string" && typeof proposal.id === "string";
41
+ }
42
+ if (proposal.action === "updateGlobal") {
43
+ return typeof proposal.slug === "string" && isRecord(proposal.data);
44
+ }
45
+ return false;
46
+ };
47
+ export const createAIApplyActionEndpointHandler = (options = {})=>async (req)=>{
48
+ if (!req.user) return Response.json({
49
+ error: "Unauthorized"
50
+ }, {
51
+ status: 401
52
+ });
53
+ const body = req.json ? await req.json().catch(()=>null) : null;
54
+ const proposal = body?.proposal;
55
+ if (!proposal) return Response.json({
56
+ error: "Proposal is required"
57
+ }, {
58
+ status: 400
59
+ });
60
+ if (!verifyAIActionProposal(proposal)) return Response.json({
61
+ error: "Proposal signature is invalid or expired."
62
+ }, {
63
+ status: 400
64
+ });
65
+ if (!isActionProposal(proposal)) return Response.json({
66
+ error: "Proposal is invalid."
67
+ }, {
68
+ status: 400
69
+ });
70
+ if ("data" in proposal && containsSensitiveData(proposal.data)) return Response.json({
71
+ error: "Proposal contains sensitive fields and cannot be applied."
72
+ }, {
73
+ status: 400
74
+ });
75
+ let normalized;
76
+ try {
77
+ if (proposal.action === "updateGlobal") {
78
+ if (!isKnownGlobal(req, proposal.slug)) return Response.json({
79
+ error: "Unknown global"
80
+ }, {
81
+ status: 400
82
+ });
83
+ const globalConfig = req.payload.config.globals?.find((global)=>global.slug === proposal.slug);
84
+ normalized = normalizeDataForFields(globalConfig?.fields || [], proposal.data);
85
+ const doc = await req.payload.updateGlobal({
86
+ data: normalized.data,
87
+ overrideAccess: false,
88
+ req,
89
+ slug: proposal.slug
90
+ });
91
+ return Response.json({
92
+ doc: getAppliedDocReference(doc),
93
+ status: "applied"
94
+ });
95
+ }
96
+ if (!isAllowedCollection(req, proposal.collection, options.collections)) return Response.json({
97
+ error: "Unknown collection"
98
+ }, {
99
+ status: 400
100
+ });
101
+ if (proposal.action === "delete") {
102
+ const doc = await req.payload.delete({
103
+ collection: proposal.collection,
104
+ id: proposal.id,
105
+ overrideAccess: false,
106
+ req
107
+ });
108
+ return Response.json({
109
+ doc: getAppliedDocReference(doc),
110
+ status: "applied"
111
+ });
112
+ }
113
+ const collectionConfig = req.payload.config.collections.find((collection)=>collection.slug === proposal.collection);
114
+ normalized = normalizeAuthData(collectionConfig, normalizeDataForFields(getCollectionFields(collectionConfig), proposal.data));
115
+ if (proposal.action === "create" && isAuthCollection(collectionConfig) && !normalized.data.password) {
116
+ return Response.json({
117
+ error: "Password is required when creating a user."
118
+ }, {
119
+ status: 400
120
+ });
121
+ }
122
+ if (proposal.action === "create" && isAuthCollection(collectionConfig) && !normalized.data.email) {
123
+ return Response.json({
124
+ error: "Email is required when creating a user."
125
+ }, {
126
+ status: 400
127
+ });
128
+ }
129
+ if (proposal.action === "create") {
130
+ const doc = await req.payload.create({
131
+ collection: proposal.collection,
132
+ data: normalized.data,
133
+ overrideAccess: false,
134
+ req
135
+ });
136
+ return Response.json({
137
+ doc: getAppliedDocReference(doc),
138
+ status: "applied"
139
+ });
140
+ }
141
+ const doc = await req.payload.update({
142
+ collection: proposal.collection,
143
+ data: normalized.data,
144
+ id: proposal.id,
145
+ overrideAccess: false,
146
+ req
147
+ });
148
+ return Response.json({
149
+ doc: getAppliedDocReference(doc),
150
+ status: "applied"
151
+ });
152
+ } catch (err) {
153
+ req.payload.logger.error({
154
+ err,
155
+ msg: "AI apply action failed",
156
+ proposal: getProposalMeta(proposal)
157
+ });
158
+ return Response.json({
159
+ error: "Could not apply proposal."
160
+ }, {
161
+ status: 400
162
+ });
163
+ }
164
+ };
@@ -0,0 +1,31 @@
1
+ import type { PayloadHandler } from "payload";
2
+ import { type AIActionSignature } from "../ai/proposals.js";
3
+ export type AIActionProposal = ({
4
+ action: "create";
5
+ collection: string;
6
+ data: Record<string, unknown>;
7
+ label: string;
8
+ } | {
9
+ action: "delete";
10
+ collection: string;
11
+ id: string;
12
+ label: string;
13
+ } | {
14
+ action: "update";
15
+ collection: string;
16
+ data: Record<string, unknown>;
17
+ id: string;
18
+ label: string;
19
+ } | {
20
+ action: "updateGlobal";
21
+ data: Record<string, unknown>;
22
+ label: string;
23
+ slug: string;
24
+ }) & {
25
+ _aiSignature?: AIActionSignature;
26
+ };
27
+ type AIChatEndpointOptions = {
28
+ collections?: string[];
29
+ };
30
+ export declare const createAIChatEndpointHandler: (options?: AIChatEndpointOptions) => PayloadHandler;
31
+ export {};
@@ -0,0 +1,297 @@
1
+ import { generateText, stepCountIs } from "ai";
2
+ import { z } from "zod";
3
+ import { signAIActionProposal } from "../ai/proposals.js";
4
+ import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
5
+ import { isAIProvider } from "../ai/providerOptions.js";
6
+ import { containsSensitiveData } from "../ai/sensitiveData.js";
7
+ import { buildPromptWithMentionContext, collectBlocks, describeField, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
8
+ export const createAIChatEndpointHandler = (options = {})=>async (req)=>{
9
+ if (!req.user) return Response.json({
10
+ error: "Unauthorized"
11
+ }, {
12
+ status: 401
13
+ });
14
+ const body = req.json ? await req.json().catch(()=>null) : null;
15
+ const prompt = body?.prompt?.trim();
16
+ if (!prompt) return Response.json({
17
+ error: "Prompt is required"
18
+ }, {
19
+ status: 400
20
+ });
21
+ const user = req.user;
22
+ const requestedProvider = body?.provider || user.aiProvider || "openai";
23
+ if (!isAIProvider(requestedProvider)) return Response.json({
24
+ error: `Unsupported AI provider: ${requestedProvider}`
25
+ }, {
26
+ status: 400
27
+ });
28
+ const provider = requestedProvider;
29
+ const providerConfig = getProviderConfig({
30
+ apiKey: user.aiApiKey,
31
+ model: body?.model,
32
+ provider
33
+ });
34
+ const debug = {
35
+ model: providerConfig.modelID,
36
+ provider,
37
+ tools: [
38
+ "getDoc",
39
+ "getGlobal",
40
+ "listCollections",
41
+ "listGlobals",
42
+ "proposeCreateDoc",
43
+ "proposeDeleteDoc",
44
+ "proposeUpdateDoc",
45
+ "proposeUpdateGlobal",
46
+ "searchDocs"
47
+ ]
48
+ };
49
+ if (!providerConfig.apiKey) {
50
+ return Response.json({
51
+ error: `Add a ${provider} API key to your account settings first.`
52
+ }, {
53
+ status: 400
54
+ });
55
+ }
56
+ try {
57
+ const proposals = [];
58
+ const addSignedProposal = (proposal)=>{
59
+ if ("data" in proposal && containsSensitiveData(proposal.data)) {
60
+ return {
61
+ error: "Proposal contains sensitive fields and cannot be created."
62
+ };
63
+ }
64
+ const signedProposal = signAIActionProposal(proposal);
65
+ proposals.push(signedProposal);
66
+ return signedProposal;
67
+ };
68
+ const collectionSlugs = getAllowedCollectionSlugs(req, options.collections);
69
+ const globalSlugs = req.payload.config.globals?.map((global)=>global.slug) || [];
70
+ const allowedCollections = req.payload.config.collections.filter((collection)=>collectionSlugs.includes(collection.slug));
71
+ if (collectionSlugs.length === 0) {
72
+ return Response.json({
73
+ error: "No AI-enabled collections are configured."
74
+ }, {
75
+ status: 400
76
+ });
77
+ }
78
+ const blockContexts = [
79
+ ...allowedCollections.flatMap((collection)=>collectBlocks({
80
+ fields: collection.fields,
81
+ parent: collection.slug
82
+ })),
83
+ ...req.payload.config.globals?.flatMap((global)=>collectBlocks({
84
+ fields: global.fields,
85
+ parent: global.slug
86
+ })) || []
87
+ ];
88
+ const mentionContext = await getMentionContext({
89
+ blockContexts,
90
+ collectionSlugs,
91
+ globalSlugs,
92
+ mentions: body?.mentions,
93
+ req
94
+ });
95
+ const collectionSlugSchema = z.enum(collectionSlugs);
96
+ const tools = {
97
+ getDoc: {
98
+ description: "Read one document by collection slug and document id.",
99
+ inputSchema: z.object({
100
+ collection: collectionSlugSchema,
101
+ id: z.string().min(1)
102
+ }),
103
+ execute: async ({ collection, id })=>{
104
+ return req.payload.findByID({
105
+ collection: collection,
106
+ depth: 2,
107
+ id,
108
+ overrideAccess: false,
109
+ req
110
+ });
111
+ }
112
+ },
113
+ listCollections: {
114
+ description: "List all Payload CMS collections available in this app.",
115
+ inputSchema: z.object({}),
116
+ execute: async ()=>{
117
+ return allowedCollections.map((collection)=>({
118
+ fields: collection.fields.map(describeField),
119
+ label: collection.labels?.plural || collection.slug,
120
+ slug: collection.slug
121
+ }));
122
+ }
123
+ },
124
+ getGlobal: {
125
+ description: "Read one Payload CMS global by slug.",
126
+ inputSchema: z.object({
127
+ slug: z.string().min(1)
128
+ }),
129
+ execute: async ({ slug })=>{
130
+ const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
131
+ if (!globalConfig) return {
132
+ error: `Unknown global: ${slug}`
133
+ };
134
+ return req.payload.findGlobal({
135
+ depth: 2,
136
+ overrideAccess: false,
137
+ req,
138
+ slug: slug
139
+ });
140
+ }
141
+ },
142
+ listGlobals: {
143
+ description: "List all Payload CMS globals available in this app.",
144
+ inputSchema: z.object({}),
145
+ execute: async ()=>{
146
+ return req.payload.config.globals?.map((global)=>({
147
+ fields: global.fields.map(describeField),
148
+ label: global.label || global.slug,
149
+ slug: global.slug
150
+ })) || [];
151
+ }
152
+ },
153
+ proposeCreateDoc: {
154
+ description: "Prepare a CMS document creation 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.",
155
+ inputSchema: z.object({
156
+ collection: collectionSlugSchema,
157
+ data: z.record(z.string(), z.unknown()),
158
+ label: z.string().min(1)
159
+ }),
160
+ execute: async ({ collection, data, label })=>{
161
+ const proposal = {
162
+ action: "create",
163
+ collection,
164
+ data,
165
+ label
166
+ };
167
+ return addSignedProposal(proposal);
168
+ }
169
+ },
170
+ proposeDeleteDoc: {
171
+ description: "Prepare a CMS document deletion proposal. This does not write to the database.",
172
+ inputSchema: z.object({
173
+ collection: collectionSlugSchema,
174
+ id: z.string().min(1),
175
+ label: z.string().min(1)
176
+ }),
177
+ execute: async ({ collection, id, label })=>{
178
+ const proposal = {
179
+ action: "delete",
180
+ collection,
181
+ id,
182
+ label
183
+ };
184
+ return addSignedProposal(proposal);
185
+ }
186
+ },
187
+ proposeUpdateDoc: {
188
+ 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.",
189
+ inputSchema: z.object({
190
+ collection: collectionSlugSchema,
191
+ data: z.record(z.string(), z.unknown()),
192
+ id: z.string().min(1),
193
+ label: z.string().min(1)
194
+ }),
195
+ execute: async ({ collection, data, id, label })=>{
196
+ const proposal = {
197
+ action: "update",
198
+ collection,
199
+ data,
200
+ id,
201
+ label
202
+ };
203
+ return addSignedProposal(proposal);
204
+ }
205
+ },
206
+ proposeUpdateGlobal: {
207
+ description: "Prepare a Payload global update proposal. This does not write to the database.",
208
+ inputSchema: z.object({
209
+ data: z.record(z.string(), z.unknown()),
210
+ label: z.string().min(1),
211
+ slug: z.string().min(1)
212
+ }),
213
+ execute: async ({ data, label, slug })=>{
214
+ const globalConfig = req.payload.config.globals?.find((global)=>global.slug === slug);
215
+ if (!globalConfig) return {
216
+ error: `Unknown global: ${slug}`
217
+ };
218
+ const proposal = {
219
+ action: "updateGlobal",
220
+ data,
221
+ label,
222
+ slug
223
+ };
224
+ return addSignedProposal(proposal);
225
+ }
226
+ },
227
+ searchDocs: {
228
+ description: "Search documents in one collection. Use query for a loose text search where possible.",
229
+ inputSchema: z.object({
230
+ collection: collectionSlugSchema,
231
+ limit: z.number().int().min(1).max(10).default(5),
232
+ query: z.string().optional()
233
+ }),
234
+ execute: async ({ collection, limit, query })=>{
235
+ const collectionConfig = allowedCollections.find((item)=>item.slug === collection);
236
+ const searchableFields = collectionConfig?.fields.filter((field)=>"name" in field && [
237
+ "email",
238
+ "text",
239
+ "textarea"
240
+ ].includes(field.type)).map((field)=>"name" in field ? field.name : null).filter(Boolean) || [];
241
+ const where = query && searchableFields.length > 0 ? {
242
+ or: searchableFields.map((field)=>({
243
+ [field]: {
244
+ contains: query
245
+ }
246
+ }))
247
+ } : undefined;
248
+ return req.payload.find({
249
+ collection: collection,
250
+ depth: 1,
251
+ limit,
252
+ overrideAccess: false,
253
+ req,
254
+ where
255
+ });
256
+ }
257
+ }
258
+ };
259
+ const result = await generateText({
260
+ maxOutputTokens: 700,
261
+ model: getModel({
262
+ apiKey: providerConfig.apiKey,
263
+ model: providerConfig.modelID,
264
+ provider
265
+ }),
266
+ prompt: buildPromptWithMentionContext({
267
+ mentionContext,
268
+ prompt
269
+ }),
270
+ stopWhen: stepCountIs(6),
271
+ system: [
272
+ "You are a CMS assistant inside Payload CMS.",
273
+ "Use tools to inspect CMS schema and content before answering content questions.",
274
+ "When mention context is provided, use it as the selected CMS scope and prefer it over guessing collection names.",
275
+ "Never claim that a write has been applied unless the user confirms an action proposal in the UI.",
276
+ "For create, update, and delete requests, call the proposal tools instead of directly changing data.",
277
+ "Keep the visible response under 80 words. Put concrete changes in proposal tool calls instead of long prose."
278
+ ].join("\n"),
279
+ tools
280
+ });
281
+ return Response.json({
282
+ proposals,
283
+ text: result.text
284
+ });
285
+ } catch (err) {
286
+ req.payload.logger.error({
287
+ debug,
288
+ err,
289
+ msg: "AI chat request failed"
290
+ });
291
+ return Response.json({
292
+ error: "AI request failed."
293
+ }, {
294
+ status: 500
295
+ });
296
+ }
297
+ };
@@ -0,0 +1,6 @@
1
+ import type { PayloadHandler } from "payload";
2
+ type AIMentionSuggestionsEndpointOptions = {
3
+ collections?: string[];
4
+ };
5
+ export declare const createAIMentionSuggestionsEndpointHandler: (options?: AIMentionSuggestionsEndpointOptions) => PayloadHandler;
6
+ export {};
@@ -0,0 +1,63 @@
1
+ import { isInternalCollection } from "../payload/shared.js";
2
+ const isAllowedCollectionSlug = (slug, collections)=>{
3
+ if (isInternalCollection(slug)) return false;
4
+ if (!collections) return true;
5
+ return collections.includes(slug);
6
+ };
7
+ const getDocLabel = (doc, useAsTitle)=>{
8
+ const titleField = useAsTitle && typeof doc[useAsTitle] === "string" ? doc[useAsTitle] : null;
9
+ return (titleField || doc.title || doc.name || doc.email || doc.id || "Untitled").toString();
10
+ };
11
+ export const createAIMentionSuggestionsEndpointHandler = (options = {})=>async (req)=>{
12
+ if (!req.user) return Response.json({
13
+ error: "Unauthorized"
14
+ }, {
15
+ status: 401
16
+ });
17
+ const body = req.json ? await req.json().catch(()=>null) : null;
18
+ const query = body?.query?.trim();
19
+ const collectionSlug = body?.collectionSlug?.trim();
20
+ if (!query && !collectionSlug) return Response.json({
21
+ suggestions: []
22
+ });
23
+ const suggestions = [];
24
+ const collections = req.payload.config.collections.filter((collection)=>{
25
+ if (!isAllowedCollectionSlug(collection.slug, options.collections)) return false;
26
+ if (collectionSlug) return collection.slug === collectionSlug;
27
+ return true;
28
+ });
29
+ for (const collection of collections){
30
+ const searchableFields = collection.fields.filter((field)=>"name" in field && [
31
+ "email",
32
+ "text",
33
+ "textarea"
34
+ ].includes(field.type)).map((field)=>"name" in field ? field.name : null).filter(Boolean);
35
+ if (query && searchableFields.length === 0) continue;
36
+ const result = await req.payload.find({
37
+ collection: collection.slug,
38
+ depth: 0,
39
+ limit: 3,
40
+ overrideAccess: false,
41
+ req,
42
+ where: query && searchableFields.length > 0 ? {
43
+ or: searchableFields.map((field)=>({
44
+ [field]: {
45
+ contains: query
46
+ }
47
+ }))
48
+ } : undefined
49
+ });
50
+ for (const doc of result.docs){
51
+ suggestions.push({
52
+ collection: collection.slug,
53
+ id: doc.id?.toString(),
54
+ label: getDocLabel(doc, collection.admin?.useAsTitle),
55
+ slug: `${collection.slug}:${doc.id?.toString()}`,
56
+ type: "doc"
57
+ });
58
+ }
59
+ }
60
+ return Response.json({
61
+ suggestions: suggestions.slice(0, 5)
62
+ });
63
+ };
@@ -0,0 +1,2 @@
1
+ export { AIInput } from "../components/AIInput.js";
2
+ export { AIApiKeyField } from "../components/AIApiKeyField.js";
@@ -0,0 +1,2 @@
1
+ export { AIInput } from "../components/AIInput.js";
2
+ export { AIApiKeyField } from "../components/AIApiKeyField.js";
@@ -0,0 +1,9 @@
1
+ import type { CollectionSlug, Config } from "payload";
2
+ import { type AIModelConfig } from "./ai/providerOptions.js";
3
+ export type PayloadAiPluginOptions = {
4
+ collections?: Partial<Record<CollectionSlug, true>>;
5
+ disabled?: boolean;
6
+ models?: AIModelConfig;
7
+ };
8
+ export type PayloadAiPluginConfig = PayloadAiPluginOptions;
9
+ export declare const payloadAiPlugin: (pluginOptions: PayloadAiPluginOptions) => (config: Config) => Config;
package/dist/index.js ADDED
@@ -0,0 +1,75 @@
1
+ import { aiProviders, getResolvedAIModelConfig } from "./ai/providerOptions.js";
2
+ import { createAIApplyActionEndpointHandler } from "./endpoints/aiApplyActionEndpointHandler.js";
3
+ import { createAIChatEndpointHandler } from "./endpoints/aiChatEndpointHandler.js";
4
+ import { createAIMentionSuggestionsEndpointHandler } from "./endpoints/aiMentionSuggestionsEndpointHandler.js";
5
+ const getEnabledCollections = (collections)=>{
6
+ if (!collections) return undefined;
7
+ return Object.entries(collections).filter(([, enabled])=>enabled).map(([slug])=>slug);
8
+ };
9
+ const addAccountFields = (config)=>{
10
+ const adminUserSlug = config.admin?.user;
11
+ if (!adminUserSlug || !config.collections) return;
12
+ const userCollection = config.collections.find((c)=>c.slug === adminUserSlug);
13
+ if (!userCollection) return;
14
+ userCollection.fields.push({
15
+ name: "aiProvider",
16
+ type: "select",
17
+ defaultValue: "openai",
18
+ options: aiProviders
19
+ }, {
20
+ name: "aiApiKey",
21
+ type: "text",
22
+ admin: {
23
+ components: {
24
+ Field: "payload-ai-plugin/client#AIApiKeyField"
25
+ }
26
+ }
27
+ });
28
+ };
29
+ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
30
+ const incomingOnInit = config.onInit;
31
+ const enabledCollections = getEnabledCollections(pluginOptions.collections);
32
+ const modelConfig = getResolvedAIModelConfig(pluginOptions.models);
33
+ if (!config.collections) config.collections = [];
34
+ addAccountFields(config);
35
+ if (pluginOptions.disabled) return config;
36
+ if (!config.endpoints) config.endpoints = [];
37
+ if (!config.admin) config.admin = {};
38
+ config.admin.custom = {
39
+ ...config.admin.custom || {},
40
+ payloadAiPlugin: {
41
+ ...config.admin.custom?.payloadAiPlugin || {},
42
+ models: modelConfig
43
+ }
44
+ };
45
+ if (!config.admin.components) config.admin.components = {};
46
+ if (!config.admin.components.beforeDashboard) config.admin.components.beforeDashboard = [];
47
+ config.admin.components.beforeDashboard.push(`payload-ai-plugin/client#AIInput`);
48
+ config.endpoints.push({
49
+ handler: createAIChatEndpointHandler({
50
+ collections: enabledCollections
51
+ }),
52
+ method: "post",
53
+ path: "/ai-chat"
54
+ });
55
+ config.endpoints.push({
56
+ handler: createAIApplyActionEndpointHandler({
57
+ collections: enabledCollections
58
+ }),
59
+ method: "post",
60
+ path: "/ai-apply-action"
61
+ });
62
+ config.endpoints.push({
63
+ handler: createAIMentionSuggestionsEndpointHandler({
64
+ collections: enabledCollections
65
+ }),
66
+ method: "post",
67
+ path: "/ai-mention-suggestions"
68
+ });
69
+ if (incomingOnInit) {
70
+ config.onInit = async (payload)=>{
71
+ await incomingOnInit(payload);
72
+ };
73
+ }
74
+ return config;
75
+ };