payload-forgeai 0.2.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.
package/README.md ADDED
@@ -0,0 +1,415 @@
1
+ # Payload Forge AI
2
+
3
+ AI-powered field generation plugin for [Payload CMS](https://payloadcms.com). Automatically generate field content using OpenAI, with support for context-aware generation based on other fields in your document.
4
+
5
+ ## Features
6
+
7
+ - 🤖 AI-powered field generation using OpenAI
8
+ - 🔗 Context-aware generation using other field values
9
+ - 📝 Custom prompts per field
10
+ - 🎯 Support for relationships, selects, and all field types
11
+ - ⚡ Simple integration with existing Payload collections
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install payload-forge-ai
17
+ # or
18
+ pnpm add payload-forge-ai
19
+ # or
20
+ yarn add payload-forge-ai
21
+ ```
22
+
23
+ ## Configuration
24
+
25
+ Add the plugin to your `payload.config.ts`:
26
+
27
+ ```ts
28
+ import { payloadForgeAi } from 'payload-forge-ai'
29
+
30
+ export default buildConfig({
31
+ plugins: [
32
+ payloadForgeAi({
33
+ apiKey: process.env.OPENAI_API_KEY,
34
+ collections: {
35
+ posts: {
36
+ fields: [
37
+ {
38
+ name: 'title',
39
+ prompt: 'Write a compelling title for this blog post',
40
+ },
41
+ {
42
+ name: 'content',
43
+ context: ['title', 'category'],
44
+ prompt: 'Write engaging content for this blog post',
45
+ },
46
+ ],
47
+ },
48
+ },
49
+ }),
50
+ ],
51
+ })
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ### Basic Field Generation
57
+
58
+ Configure a field for AI generation:
59
+
60
+ ```ts
61
+ {
62
+ name: 'title',
63
+ prompt: 'Write a catchy title that is max 60 characters',
64
+ }
65
+ ```
66
+
67
+ The plugin automatically includes the current field value in the context, instructing the AI to generate something different when regenerating. This is useful for getting variations.
68
+
69
+ To disable this behavior and allow the AI to consider the current value without the "generate different" instruction:
70
+
71
+ ```ts
72
+ {
73
+ name: 'title',
74
+ prompt: 'Write a catchy title that is max 60 characters',
75
+ generateDifferent: false, // AI can generate similar content
76
+ }
77
+ ```
78
+
79
+ ### Context-Aware Generation
80
+
81
+ Use other fields as context for more relevant AI generation:
82
+
83
+ #### Simple Context (by field name)
84
+
85
+ ```ts
86
+ {
87
+ name: 'content',
88
+ context: ['title', 'category'],
89
+ prompt: 'Write the main content for this article',
90
+ }
91
+ ```
92
+
93
+ #### Custom Context Prompts
94
+
95
+ Provide custom prompts for how each context field should be used:
96
+
97
+ ```ts
98
+ {
99
+ name: 'content',
100
+ context: [
101
+ {
102
+ field: 'title',
103
+ prompt: 'The article title is',
104
+ },
105
+ {
106
+ field: 'category',
107
+ prompt: 'The category for this post is',
108
+ },
109
+ {
110
+ field: 'author',
111
+ prompt: 'Written by author',
112
+ },
113
+ ],
114
+ prompt: 'Write engaging content based on the context above',
115
+ }
116
+ ```
117
+
118
+ ### Working with Relationships
119
+
120
+ The plugin automatically fetches and populates relationship fields:
121
+
122
+ ```ts
123
+ // In your collection
124
+ fields: [
125
+ {
126
+ name: 'author',
127
+ type: 'relationship',
128
+ relationTo: 'users',
129
+ },
130
+ {
131
+ name: 'bio',
132
+ type: 'textarea',
133
+ },
134
+ ]
135
+
136
+ // In your AI config - Use full author object
137
+ {
138
+ name: 'bio',
139
+ context: [
140
+ {
141
+ field: 'author',
142
+ prompt: 'Author details',
143
+ },
144
+ ],
145
+ prompt: 'Write a professional bio',
146
+ }
147
+
148
+ // Or extract specific fields from the relationship
149
+ {
150
+ name: 'bio',
151
+ context: [
152
+ {
153
+ field: 'author',
154
+ fields: ['name', 'email'], // Only include name and email
155
+ prompt: 'Author details',
156
+ },
157
+ ],
158
+ prompt: 'Write a professional bio and mention the author name',
159
+ }
160
+
161
+ // Use nested prompts - BOTH outer and inner prompts are used
162
+ {
163
+ name: 'content',
164
+ context: [
165
+ {
166
+ field: 'author',
167
+ prompt: 'The author information', // Outer prompt - used as header
168
+ fields: [
169
+ {
170
+ field: 'name',
171
+ prompt: 'Written by', // Inner prompt - used for this specific field
172
+ },
173
+ {
174
+ field: 'email',
175
+ prompt: 'Contact', // Inner prompt - used for this specific field
176
+ },
177
+ ],
178
+ },
179
+ ],
180
+ prompt: 'Write content with author attribution',
181
+ }
182
+ // AI receives:
183
+ // The author information:
184
+ // Written by: John Doe
185
+ // Contact: john@example.com
186
+ ```
187
+
188
+ #### Selecting Specific Fields
189
+
190
+ Use the `fields` array to extract only specific properties from context:
191
+
192
+ ```ts
193
+ {
194
+ name: 'summary',
195
+ context: [
196
+ {
197
+ field: 'author',
198
+ fields: ['name'], // Only include the author's name
199
+ prompt: 'Written by',
200
+ },
201
+ {
202
+ field: 'metadata',
203
+ fields: ['tags', 'category'], // Extract specific nested properties
204
+ },
205
+ ],
206
+ prompt: 'Write a summary with author attribution',
207
+ }
208
+ ```
209
+
210
+ #### Nested Field Selection
211
+
212
+ For complex nested objects, you can use recursive field configuration with prompts at each level:
213
+
214
+ ```ts
215
+ {
216
+ name: 'description',
217
+ context: [
218
+ {
219
+ field: 'product',
220
+ prompt: 'Product details', // Outer prompt (used as fallback for nested fields without prompts)
221
+ fields: [
222
+ 'name', // Will use: "Product details - name: value"
223
+ 'price',
224
+ {
225
+ field: 'manufacturer',
226
+ prompt: 'Made by', // This prompt overrides the parent
227
+ fields: ['name', 'country'], // Will use: "Made by - name: value" and "Made by - country: value"
228
+ },
229
+ {
230
+ field: 'specs',
231
+ fields: [
232
+ {
233
+ field: 'weight',
234
+ prompt: 'Product weight', // Custom prompt for this specific field
235
+ },
236
+ ],
237
+ },
238
+ ],
239
+ },
240
+ ],
241
+ prompt: 'Write a product description',
242
+ }
243
+ ```
244
+
245
+ **How nested prompts work:**
246
+ - **ALL prompts are used** - both outer and nested prompts are included in the context
247
+ - Outer prompts act as section headers/introductions
248
+ - Nested prompts describe individual fields
249
+ - Example output:
250
+ ```
251
+ Product details:
252
+ name: Widget X
253
+ Made by:
254
+ name: ACME Corp
255
+ country: USA
256
+ Product weight: 2.5kg
257
+ ```
258
+ - This allows fine-grained control over how each piece of data is presented to the AI
259
+
260
+ ## Configuration Options
261
+
262
+ ### Plugin Options
263
+
264
+ ```ts
265
+ type PayloadForgeAiConfig = {
266
+ apiKey: string // Your OpenAI API key
267
+ disabled?: boolean // Disable the plugin
268
+ collections?: {
269
+ [collectionSlug: string]: {
270
+ fields: PayloadForgeAiFieldConfig[]
271
+ }
272
+ }
273
+ }
274
+ ```
275
+
276
+ ### Field Configuration
277
+
278
+ ```ts
279
+ type PayloadForgeAiContextField =
280
+ | string // Simple field name
281
+ | {
282
+ field: string // Field name to use as context
283
+ prompt?: string // Custom prompt for this context field
284
+ fields?: PayloadForgeAiContextField[] // Recursively extract nested fields
285
+ }
286
+
287
+ type PayloadForgeAiFieldConfig = {
288
+ name: string // Field name to enable AI generation
289
+ prompt: string // Instructions for the AI
290
+ context?: PayloadForgeAiContextField[] // Fields to use as context
291
+ generateDifferent?: boolean // If true (default), instructs AI to generate different from current value
292
+ }
293
+ ```
294
+
295
+ ## Examples
296
+
297
+ ### Blog Post with Category and Author Context
298
+
299
+ ```ts
300
+ payloadForgeAi({
301
+ apiKey: process.env.OPENAI_API_KEY,
302
+ collections: {
303
+ posts: {
304
+ fields: [
305
+ {
306
+ name: 'title',
307
+ context: ['category'],
308
+ prompt: 'Write a title (max 8 words)',
309
+ },
310
+ {
311
+ name: 'excerpt',
312
+ context: ['title', 'category'],
313
+ prompt: 'Write a brief excerpt (max 150 characters)',
314
+ },
315
+ {
316
+ name: 'content',
317
+ context: [
318
+ { field: 'title', prompt: 'Article title' },
319
+ { field: 'category', prompt: 'Category' },
320
+ { field: 'excerpt', prompt: 'Summary' },
321
+ {
322
+ field: 'author',
323
+ fields: ['name'], // Only use author's name
324
+ prompt: 'Written by'
325
+ },
326
+ ],
327
+ prompt: 'Write comprehensive content (300-500 words) and include author attribution at the end',
328
+ },
329
+ ],
330
+ },
331
+ },
332
+ })
333
+ ```
334
+
335
+ ### Product Description with Related Data
336
+
337
+ ```ts
338
+ payloadForgeAi({
339
+ apiKey: process.env.OPENAI_API_KEY,
340
+ collections: {
341
+ products: {
342
+ fields: [
343
+ {
344
+ name: 'description',
345
+ context: [
346
+ { field: 'name', prompt: 'Product name' },
347
+ { field: 'category', prompt: 'Category' },
348
+ { field: 'price', prompt: 'Price' },
349
+ { field: 'features', prompt: 'Key features' },
350
+ ],
351
+ prompt: 'Write a compelling product description',
352
+ },
353
+ {
354
+ name: 'seoTitle',
355
+ context: ['name', 'category'],
356
+ prompt: 'Write an SEO-optimized title (max 60 chars)',
357
+ },
358
+ {
359
+ name: 'seoDescription',
360
+ context: ['name', 'description'],
361
+ prompt: 'Write an SEO meta description (max 160 chars)',
362
+ },
363
+ ],
364
+ },
365
+ },
366
+ })
367
+ ```
368
+
369
+ ## Environment Variables
370
+
371
+ Create a `.env` file in your project:
372
+
373
+ ```env
374
+ OPENAI_API_KEY=your_openai_api_key_here
375
+ ```
376
+
377
+ ## How It Works
378
+
379
+ 1. The plugin adds a "Generate" button to configured fields in the Payload admin UI
380
+ 2. When clicked, it gathers context from other fields in the document
381
+ 3. Includes the current field value (if any) and instructs the AI to generate something different (configurable)
382
+ 4. Sends the context and prompt to OpenAI via your API endpoint
383
+ 5. Returns the generated content and populates the field
384
+
385
+ ## Development
386
+
387
+ ### Running the Dev Environment
388
+
389
+ ```bash
390
+ # Install dependencies
391
+ pnpm install
392
+
393
+ # Create .env file
394
+ cp .env.example .env
395
+
396
+ # Add your OpenAI API key to .env
397
+ # OPENAI_API_KEY=your_key_here
398
+
399
+ # Start the dev server
400
+ pnpm dev
401
+ ```
402
+
403
+ ### Testing
404
+
405
+ ```bash
406
+ pnpm test
407
+ ```
408
+
409
+ ## License
410
+
411
+ MIT
412
+
413
+ ## Support
414
+
415
+ For issues and questions, please open an issue on GitHub.
@@ -0,0 +1 @@
1
+ export declare const BeforeDashboardClient: () => import("react").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import type { ServerComponentProps } from 'payload';
2
+ export declare const BeforeDashboardServer: (props: ServerComponentProps) => Promise<import("react").JSX.Element>;
@@ -0,0 +1,5 @@
1
+ .wrapper {
2
+ display: flex;
3
+ gap: 5px;
4
+ flex-direction: column;
5
+ }
@@ -0,0 +1,2 @@
1
+ import { type TextFieldClientComponent } from 'payload';
2
+ export declare const ForgeAIField: TextFieldClientComponent;
@@ -0,0 +1,164 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { useConfig, useDocumentInfo, useField, FieldLabel, useFormFields } from '@payloadcms/ui';
5
+ export const ForgeAIField = (props)=>{
6
+ const { path, field, readOnly } = props;
7
+ const { setValue, value, errorMessage, showError, formProcessing } = useField({
8
+ path
9
+ });
10
+ const { collectionSlug, id: docId } = useDocumentInfo();
11
+ const { config } = useConfig();
12
+ // Access all form fields for context
13
+ const allFields = useFormFields(([fields])=>fields);
14
+ const [isGenerating, setIsGenerating] = useState(false);
15
+ const [genError, setGenError] = useState(null);
16
+ const endpoint = useMemo(()=>{
17
+ if (!collectionSlug) return null;
18
+ const base = config?.serverURL || '';
19
+ return `${base}/api/forge-ai/${collectionSlug}/${path}`;
20
+ }, [
21
+ collectionSlug,
22
+ config?.serverURL,
23
+ path
24
+ ]);
25
+ const generate = useCallback(async ()=>{
26
+ if (!endpoint) return;
27
+ setIsGenerating(true);
28
+ setGenError(null);
29
+ try {
30
+ // Gather context from form fields
31
+ const contextData = {};
32
+ if (allFields) {
33
+ Object.entries(allFields).forEach(([fieldPath, fieldState])=>{
34
+ contextData[fieldPath] = fieldState.value;
35
+ });
36
+ }
37
+ const res = await fetch(endpoint, {
38
+ method: 'POST',
39
+ credentials: 'include',
40
+ headers: {
41
+ 'Content-Type': 'application/json'
42
+ },
43
+ body: JSON.stringify({
44
+ context: contextData,
45
+ documentId: docId,
46
+ currentValue: value
47
+ })
48
+ });
49
+ if (!res.ok) throw new Error(await res.text());
50
+ const generated = await res.json();
51
+ setValue(generated);
52
+ } catch (e) {
53
+ setGenError(e instanceof Error ? e.message : String(e));
54
+ } finally{
55
+ setIsGenerating(false);
56
+ }
57
+ }, [
58
+ endpoint,
59
+ setValue,
60
+ allFields,
61
+ docId
62
+ ]);
63
+ return /*#__PURE__*/ _jsxs("div", {
64
+ className: "field-type",
65
+ children: [
66
+ /*#__PURE__*/ _jsx(FieldLabel, {
67
+ htmlFor: path,
68
+ label: field.label ?? field.name,
69
+ required: field.required
70
+ }),
71
+ /*#__PURE__*/ _jsxs("div", {
72
+ children: [
73
+ /*#__PURE__*/ _jsx("input", {
74
+ id: path,
75
+ style: {
76
+ width: '100%'
77
+ },
78
+ disabled: readOnly || formProcessing,
79
+ value: value ?? '',
80
+ onChange: (e)=>setValue(e.target.value)
81
+ }),
82
+ /*#__PURE__*/ _jsxs("button", {
83
+ type: "button",
84
+ onClick: generate,
85
+ disabled: readOnly || formProcessing || isGenerating || !endpoint,
86
+ style: {
87
+ marginTop: 8,
88
+ padding: '6px 12px',
89
+ border: '1px solid var(--theme-elevation-400)',
90
+ borderRadius: 4,
91
+ background: 'var(--theme-elevation-50)',
92
+ cursor: readOnly || formProcessing || isGenerating || !endpoint ? 'not-allowed' : 'pointer',
93
+ display: 'inline-flex',
94
+ alignItems: 'center',
95
+ gap: 6,
96
+ fontSize: 13,
97
+ color: 'var(--theme-elevation-800)',
98
+ transition: 'all 0.2s ease',
99
+ opacity: readOnly || formProcessing || !endpoint ? 0.5 : 1
100
+ },
101
+ onMouseEnter: (e)=>{
102
+ if (!readOnly && !formProcessing && !isGenerating && endpoint) {
103
+ e.currentTarget.style.background = 'var(--theme-elevation-100)';
104
+ e.currentTarget.style.borderColor = 'var(--theme-elevation-500)';
105
+ }
106
+ },
107
+ onMouseLeave: (e)=>{
108
+ e.currentTarget.style.background = 'var(--theme-elevation-50)';
109
+ e.currentTarget.style.borderColor = 'var(--theme-elevation-400)';
110
+ },
111
+ children: [
112
+ /*#__PURE__*/ _jsx("svg", {
113
+ xmlns: "http://www.w3.org/2000/svg",
114
+ width: "16",
115
+ height: "16",
116
+ viewBox: "0 0 24 24",
117
+ fill: "none",
118
+ stroke: "currentColor",
119
+ strokeWidth: "2",
120
+ strokeLinecap: "round",
121
+ strokeLinejoin: "round",
122
+ style: {
123
+ animation: isGenerating ? 'spin 1s linear infinite' : 'none'
124
+ },
125
+ children: /*#__PURE__*/ _jsx("path", {
126
+ d: "M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"
127
+ })
128
+ }),
129
+ isGenerating ? 'Generating…' : 'Generate with AI'
130
+ ]
131
+ }),
132
+ /*#__PURE__*/ _jsx("style", {
133
+ children: `
134
+ @keyframes spin {
135
+ from {
136
+ transform: rotate(0deg);
137
+ }
138
+ to {
139
+ transform: rotate(360deg);
140
+ }
141
+ }
142
+ `
143
+ })
144
+ ]
145
+ }),
146
+ genError ? /*#__PURE__*/ _jsx("div", {
147
+ style: {
148
+ marginTop: 6,
149
+ color: 'var(--theme-error-500)'
150
+ },
151
+ children: genError
152
+ }) : null,
153
+ showError && errorMessage ? /*#__PURE__*/ _jsx("div", {
154
+ style: {
155
+ marginTop: 6,
156
+ color: 'var(--theme-error-500)'
157
+ },
158
+ children: errorMessage
159
+ }) : null
160
+ ]
161
+ });
162
+ };
163
+
164
+ //# sourceMappingURL=ForgeAIField.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/ForgeAIField.tsx"],"sourcesContent":["'use client'\n\nimport React, { useCallback, useMemo, useState } from 'react'\nimport { type TextFieldClientComponent } from 'payload'\nimport { useConfig, useDocumentInfo, useField, FieldLabel, useFormFields } from '@payloadcms/ui'\n\nexport const ForgeAIField: TextFieldClientComponent = (props) => {\n const { path, field, readOnly } = props\n\n const { setValue, value, errorMessage, showError, formProcessing } = useField<string>({ path })\n const { collectionSlug, id: docId } = useDocumentInfo()\n const { config } = useConfig()\n \n // Access all form fields for context\n const allFields = useFormFields(([fields]) => fields)\n\n const [isGenerating, setIsGenerating] = useState(false)\n const [genError, setGenError] = useState<string | null>(null)\n\n const endpoint = useMemo(() => {\n if (!collectionSlug) return null\n const base = config?.serverURL || ''\n return `${base}/api/forge-ai/${collectionSlug}/${path}`\n }, [collectionSlug, config?.serverURL, path])\n\n const generate = useCallback(async () => {\n if (!endpoint) return\n setIsGenerating(true)\n setGenError(null)\n try {\n // Gather context from form fields\n const contextData: Record<string, any> = {}\n if (allFields) {\n Object.entries(allFields).forEach(([fieldPath, fieldState]) => {\n contextData[fieldPath] = fieldState.value\n })\n }\n\n const res = await fetch(endpoint, {\n method: 'POST',\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n context: contextData,\n documentId: docId,\n currentValue: value, // Include current value so AI can generate something different\n }),\n })\n\n if (!res.ok) throw new Error(await res.text())\n\n const generated = await res.json()\n setValue(generated)\n } catch (e) {\n setGenError(e instanceof Error ? e.message : String(e))\n } finally {\n setIsGenerating(false)\n }\n }, [endpoint, setValue, allFields, docId])\n\n return (\n <div className=\"field-type\">\n <FieldLabel htmlFor={path} label={field.label ?? field.name} required={field.required} />\n\n <div>\n <input\n id={path}\n style={{ width: '100%' }}\n disabled={readOnly || formProcessing}\n value={value ?? ''}\n onChange={(e) => setValue(e.target.value)}\n />\n\n <button\n type=\"button\"\n onClick={generate}\n disabled={readOnly || formProcessing || isGenerating || !endpoint}\n style={{\n marginTop: 8,\n padding: '6px 12px',\n border: '1px solid var(--theme-elevation-400)',\n borderRadius: 4,\n background: 'var(--theme-elevation-50)',\n cursor: readOnly || formProcessing || isGenerating || !endpoint ? 'not-allowed' : 'pointer',\n display: 'inline-flex',\n alignItems: 'center',\n gap: 6,\n fontSize: 13,\n color: 'var(--theme-elevation-800)',\n transition: 'all 0.2s ease',\n opacity: readOnly || formProcessing || !endpoint ? 0.5 : 1,\n }}\n onMouseEnter={(e) => {\n if (!readOnly && !formProcessing && !isGenerating && endpoint) {\n e.currentTarget.style.background = 'var(--theme-elevation-100)'\n e.currentTarget.style.borderColor = 'var(--theme-elevation-500)'\n }\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = 'var(--theme-elevation-50)'\n e.currentTarget.style.borderColor = 'var(--theme-elevation-400)'\n }}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n style={{\n animation: isGenerating ? 'spin 1s linear infinite' : 'none',\n }}\n >\n <path d=\"M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2\" />\n </svg>\n {isGenerating ? 'Generating…' : 'Generate with AI'}\n </button>\n\n <style>{`\n @keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n `}</style>\n </div>\n\n {genError ? (\n <div style={{ marginTop: 6, color: 'var(--theme-error-500)' }}>{genError}</div>\n ) : null}\n {showError && errorMessage ? (\n <div style={{ marginTop: 6, color: 'var(--theme-error-500)' }}>{errorMessage}</div>\n ) : null}\n </div>\n )\n}\n"],"names":["React","useCallback","useMemo","useState","useConfig","useDocumentInfo","useField","FieldLabel","useFormFields","ForgeAIField","props","path","field","readOnly","setValue","value","errorMessage","showError","formProcessing","collectionSlug","id","docId","config","allFields","fields","isGenerating","setIsGenerating","genError","setGenError","endpoint","base","serverURL","generate","contextData","Object","entries","forEach","fieldPath","fieldState","res","fetch","method","credentials","headers","body","JSON","stringify","context","documentId","currentValue","ok","Error","text","generated","json","e","message","String","div","className","htmlFor","label","name","required","input","style","width","disabled","onChange","target","button","type","onClick","marginTop","padding","border","borderRadius","background","cursor","display","alignItems","gap","fontSize","color","transition","opacity","onMouseEnter","currentTarget","borderColor","onMouseLeave","svg","xmlns","height","viewBox","fill","stroke","strokeWidth","strokeLinecap","strokeLinejoin","animation","d"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { PayloadHandler } from 'payload';
2
+ export declare const customEndpointHandler: PayloadHandler;
@@ -0,0 +1,7 @@
1
+ export const customEndpointHandler = ()=>{
2
+ return Response.json({
3
+ message: 'Hello from custom endpoint'
4
+ });
5
+ };
6
+
7
+ //# sourceMappingURL=customEndpointHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/customEndpointHandler.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\n\nexport const customEndpointHandler: PayloadHandler = () => {\n return Response.json({ message: 'Hello from custom endpoint' })\n}\n"],"names":["customEndpointHandler","Response","json","message"],"mappings":"AAEA,OAAO,MAAMA,wBAAwC;IACnD,OAAOC,SAASC,IAAI,CAAC;QAAEC,SAAS;IAA6B;AAC/D,EAAC"}
@@ -0,0 +1 @@
1
+ export { ForgeAIField } from '../components/ForgeAIField.js';
@@ -0,0 +1,3 @@
1
+ export { ForgeAIField } from '../components/ForgeAIField.js';
2
+
3
+ //# sourceMappingURL=ForgeAIField.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/exports/ForgeAIField.ts"],"sourcesContent":["export { ForgeAIField } from '../components/ForgeAIField.js'\n"],"names":["ForgeAIField"],"mappings":"AAAA,SAASA,YAAY,QAAQ,gCAA+B"}
@@ -0,0 +1 @@
1
+ export { BeforeDashboardClient } from '../components/BeforeDashboardClient.js';
@@ -0,0 +1 @@
1
+ export { BeforeDashboardServer } from '../components/BeforeDashboardServer.js';
@@ -0,0 +1,32 @@
1
+ import type { CollectionSlug, Config, Field } from 'payload';
2
+ export type PayloadForgeAiContextField = string | {
3
+ field: string;
4
+ prompt?: string;
5
+ /**
6
+ * For relationship fields, specify which fields to extract from the related document.
7
+ * For other fields, specify which nested properties to extract.
8
+ * If not specified, the entire value will be used.
9
+ * Can be nested for complex object structures.
10
+ */
11
+ fields?: PayloadForgeAiContextField[];
12
+ };
13
+ export type PayloadForgeAiFieldConfig = {
14
+ context?: PayloadForgeAiContextField[];
15
+ /**
16
+ * If true, tells the AI to generate something different from the current value.
17
+ * Defaults to true.
18
+ */
19
+ generateDifferent?: boolean;
20
+ name: string;
21
+ prompt: string;
22
+ };
23
+ export type PayloadForgeAiCollectionConfig = {
24
+ fields: PayloadForgeAiFieldConfig[];
25
+ };
26
+ export type PayloadForgeAiConfig = {
27
+ apiKey: string;
28
+ collections?: Partial<Record<CollectionSlug, PayloadForgeAiCollectionConfig>>;
29
+ disabled?: boolean;
30
+ };
31
+ export declare const payloadForgeAi: (pluginOptions: PayloadForgeAiConfig) => (config: Config) => Config;
32
+ export declare function setAdminFieldComponent<T extends Field>(field: T, fieldComponent: string): T;
package/dist/index.js ADDED
@@ -0,0 +1,309 @@
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
+ import { generateText } from 'ai';
3
+ import { flattenTopLevelFields, InvalidConfiguration } from 'payload';
4
+ function getValidFieldNames(collection) {
5
+ const flattened = flattenTopLevelFields(collection.fields);
6
+ return new Set(flattened.map((f)=>f.name).filter((name)=>name != null));
7
+ }
8
+ function validateCollectionFields(collectionSlug, collection, allowedFields) {
9
+ const validNames = getValidFieldNames(collection);
10
+ const invalid = allowedFields.filter((name)=>!validNames.has(name));
11
+ if (invalid.length > 0) {
12
+ throw new InvalidConfiguration(`payload-forge-ai: Collection "${collectionSlug}" does not have these fields: ${invalid.join(', ')}. ` + `Valid top-level fields: ${[
13
+ ...validNames
14
+ ].sort().join(', ')}.`);
15
+ }
16
+ }
17
+ /**
18
+ * Recursively extract fields from an object based on field configuration
19
+ */ function extractFields(data, fieldConfigs) {
20
+ if (!data || typeof data !== 'object') {
21
+ return data;
22
+ }
23
+ const result = {};
24
+ for (const config of fieldConfigs){
25
+ const fieldName = typeof config === 'string' ? config : config.field;
26
+ const nestedFields = typeof config === 'string' ? undefined : config.fields;
27
+ if (data[fieldName] !== undefined) {
28
+ if (nestedFields && nestedFields.length > 0) {
29
+ // Recursively extract nested fields
30
+ result[fieldName] = extractFields(data[fieldName], nestedFields);
31
+ } else {
32
+ // Just include the field value
33
+ result[fieldName] = data[fieldName];
34
+ }
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ /**
40
+ * Recursively build context parts with prompts for nested fields
41
+ */ function buildContextParts(data, fieldConfigs, indent = ' ') {
42
+ if (!data || typeof data !== 'object') {
43
+ return [];
44
+ }
45
+ const parts = [];
46
+ for (const config of fieldConfigs){
47
+ const fieldName = typeof config === 'string' ? config : config.field;
48
+ const customPrompt = typeof config === 'string' ? undefined : config.prompt;
49
+ const nestedFields = typeof config === 'string' ? undefined : config.fields;
50
+ const fieldValue = data[fieldName];
51
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
52
+ if (nestedFields && nestedFields.length > 0 && typeof fieldValue === 'object') {
53
+ // Handle nested fields with their own prompts
54
+ // Add the current field's prompt as a header if it exists
55
+ if (customPrompt) {
56
+ parts.push(`${indent}${customPrompt}:`);
57
+ }
58
+ const nestedParts = buildContextParts(fieldValue, nestedFields, indent + ' ');
59
+ parts.push(...nestedParts);
60
+ } else {
61
+ // Leaf value - format with prompt
62
+ let valueStr;
63
+ if (typeof fieldValue === 'object') {
64
+ valueStr = JSON.stringify(fieldValue, null, 2);
65
+ } else {
66
+ valueStr = String(fieldValue);
67
+ }
68
+ if (customPrompt) {
69
+ parts.push(`${indent}${customPrompt}: ${valueStr}`);
70
+ } else {
71
+ parts.push(`${indent}${fieldName}: ${valueStr}`);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ return parts;
77
+ }
78
+ export const payloadForgeAi = (pluginOptions)=>(config)=>{
79
+ if (!config.collections) {
80
+ config.collections = [];
81
+ }
82
+ if (pluginOptions.disabled) {
83
+ return config;
84
+ }
85
+ const openAi = createOpenAI({
86
+ apiKey: pluginOptions.apiKey
87
+ });
88
+ const model = openAi('gpt-5');
89
+ const systemMessages = [
90
+ {
91
+ content: `
92
+ You are a helpful assistant that can help with the creation of content in payloadCMS fields. You must return only the content and not any other questions or comments!
93
+ The content must be in the same language as the prompt.
94
+ The content must be in the same format as the field type.
95
+ The content must be in the same length as the field type.
96
+ The content must be in the same style as the field type.
97
+ The content must be in the same tone as the field type.
98
+ The content must be in the same voice as the field type.
99
+ You may vary the content to make it more interesting and engaging, but you must always return the content in the same language as the prompt.
100
+ `,
101
+ role: 'system'
102
+ }
103
+ ];
104
+ const endpoints = config.endpoints ?? [];
105
+ if (pluginOptions.collections) {
106
+ for(const collectionSlug in pluginOptions.collections){
107
+ const entry = pluginOptions.collections[collectionSlug];
108
+ if (!entry?.fields?.length) {
109
+ continue;
110
+ }
111
+ const collection = config.collections.find((c)=>c.slug === collectionSlug);
112
+ if (!collection) {
113
+ throw new InvalidConfiguration(`payload-forge-ai: Collection "${collectionSlug}" is not defined in config.collections.`);
114
+ }
115
+ const fieldNames = entry.fields.map((f)=>f.name);
116
+ validateCollectionFields(collectionSlug, collection, fieldNames);
117
+ entry.fields.forEach((field)=>{
118
+ // Validate context fields
119
+ if (field.context && field.context.length > 0) {
120
+ const contextFieldNames = field.context.map((ctx)=>typeof ctx === 'string' ? ctx : ctx.field);
121
+ validateCollectionFields(collectionSlug, collection, contextFieldNames);
122
+ }
123
+ endpoints.push({
124
+ handler: async (req)=>{
125
+ const payload = req.payload;
126
+ const flattened = flattenTopLevelFields(collection.fields);
127
+ const fieldData = flattened.find((f)=>f.name === field.name);
128
+ if (!fieldData) {
129
+ throw new InvalidConfiguration(`payload-forge-ai: Collection "${collectionSlug}" does not have field "${field.name}".`);
130
+ }
131
+ // Parse request body for context
132
+ let contextData = {};
133
+ let documentId;
134
+ let currentValue = undefined;
135
+ try {
136
+ if (req.json) {
137
+ const body = await req.json();
138
+ contextData = body.context || {};
139
+ documentId = body.documentId;
140
+ currentValue = body.currentValue;
141
+ }
142
+ } catch (e) {
143
+ // If no body or invalid JSON, continue without context
144
+ }
145
+ // Build context prompt from configured context fields
146
+ let contextPrompt = '';
147
+ if (field.context && field.context.length > 0) {
148
+ const contextParts = [];
149
+ for (const ctx of field.context){
150
+ const fieldName = typeof ctx === 'string' ? ctx : ctx.field;
151
+ const customPrompt = typeof ctx === 'string' ? undefined : ctx.prompt;
152
+ const selectedFields = typeof ctx === 'string' ? undefined : ctx.fields;
153
+ let fieldValue = contextData[fieldName];
154
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
155
+ const contextField = flattened.find((f)=>f.name === fieldName);
156
+ const fieldType = contextField?.type || 'unknown';
157
+ // Handle relationship fields - fetch the related document if we only have an ID
158
+ if (contextField?.type === 'relationship' && typeof fieldValue === 'string') {
159
+ try {
160
+ const relationTo = contextField.relationTo;
161
+ if (relationTo) {
162
+ const relatedDoc = await payload.findByID({
163
+ collection: relationTo,
164
+ id: fieldValue
165
+ });
166
+ fieldValue = relatedDoc;
167
+ }
168
+ } catch (e) {
169
+ console.error(`Failed to fetch related document for ${fieldName}:`, e);
170
+ // Continue with the ID if fetch fails
171
+ }
172
+ }
173
+ // Handle nested fields with recursive prompts
174
+ if (selectedFields && selectedFields.length > 0 && typeof fieldValue === 'object') {
175
+ // Add the outer prompt as a header if it exists
176
+ if (customPrompt) {
177
+ contextParts.push(`${customPrompt}:`);
178
+ }
179
+ console.log(`Building nested parts for field: ${fieldName}`);
180
+ console.log(`Field value keys:`, Object.keys(fieldValue));
181
+ console.log(`Selected fields config:`, JSON.stringify(selectedFields, null, 2));
182
+ const nestedParts = buildContextParts(fieldValue, selectedFields);
183
+ console.log(`Nested parts result:`, nestedParts);
184
+ contextParts.push(...nestedParts);
185
+ } else {
186
+ // Simple field without nesting
187
+ let valueStr;
188
+ if (typeof fieldValue === 'object') {
189
+ valueStr = JSON.stringify(fieldValue, null, 2);
190
+ } else {
191
+ valueStr = String(fieldValue);
192
+ }
193
+ if (customPrompt) {
194
+ contextParts.push(`${customPrompt}: ${valueStr}`);
195
+ } else {
196
+ contextParts.push(`${fieldName} (${fieldType}): ${valueStr}`);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ if (contextParts.length > 0) {
202
+ contextPrompt = '\n\nContext from other fields:\n' + contextParts.join('\n');
203
+ }
204
+ }
205
+ // Add current value info if it exists
206
+ let currentValuePrompt = '';
207
+ const shouldGenerateDifferent = field.generateDifferent ?? true // Default to true
208
+ ;
209
+ if (currentValue !== undefined && currentValue !== null && currentValue !== '') {
210
+ if (shouldGenerateDifferent) {
211
+ currentValuePrompt = `\n\nCurrent value: ${currentValue}\nIMPORTANT: Generate something DIFFERENT from the current value above. Do not repeat it.`;
212
+ } else {
213
+ currentValuePrompt = `\n\nCurrent value: ${currentValue}`;
214
+ }
215
+ }
216
+ const messages = [
217
+ ...systemMessages,
218
+ {
219
+ content: `
220
+ Collection: ${collectionSlug}
221
+ Field name: ${fieldData.name}
222
+ Field type: ${fieldData.type}
223
+ Custom instructions: ${field.prompt}${contextPrompt}${currentValuePrompt}
224
+ `,
225
+ role: 'system'
226
+ }
227
+ ];
228
+ console.log(messages[messages.length - 1].content);
229
+ const response = await generateText({
230
+ messages,
231
+ model
232
+ });
233
+ return Response.json(response.text);
234
+ },
235
+ method: 'post',
236
+ path: `/forge-ai/${collectionSlug}/${field.name}`
237
+ });
238
+ });
239
+ }
240
+ }
241
+ const collectionSlugs = Object.keys(pluginOptions?.collections ?? {});
242
+ return {
243
+ ...config,
244
+ collections: [
245
+ ...(config.collections ?? []).filter((c)=>!collectionSlugs.includes(c.slug)),
246
+ ...collectionSlugs.map((collectionSlug)=>{
247
+ const collection = config.collections?.find((c)=>c.slug === collectionSlug);
248
+ if (!collection) {
249
+ throw new InvalidConfiguration(`payload-forge-ai: Collection "${collectionSlug}" is not defined in config.collections.`);
250
+ }
251
+ return {
252
+ ...collection,
253
+ fields: [
254
+ ...collection.fields.filter((f)=>{
255
+ if (!hasName(f)) {
256
+ return true;
257
+ }
258
+ return pluginOptions.collections?.[collectionSlug]?.fields?.find((field)=>field.name === f.name) === undefined;
259
+ }).map((f)=>{
260
+ return f;
261
+ }),
262
+ ...pluginOptions.collections?.[collectionSlug]?.fields?.map((field)=>{
263
+ const fieldData = collection.fields.filter(hasName).find((f)=>f.name === field.name);
264
+ if (!fieldData) {
265
+ throw new InvalidConfiguration(`payload-forge-ai: Collection "${collectionSlug}" does not have field "${field.name}".`);
266
+ }
267
+ return setAdminFieldComponent(fieldData, 'payload-forge-ai/ForgeAIField#ForgeAIField');
268
+ }) ?? []
269
+ ]
270
+ };
271
+ })
272
+ ],
273
+ endpoints: [
274
+ ...config.endpoints || [],
275
+ ...endpoints
276
+ ]
277
+ };
278
+ // messages.push({
279
+ // content:
280
+ // 'You are a helpful assistant that can help with the creation of content for a website.',
281
+ // role: 'system',
282
+ // })
283
+ // if (!config.admin) {
284
+ // config.admin = {}
285
+ // }
286
+ // if (!config.admin.components) {
287
+ // config.admin.components = {}
288
+ // }
289
+ // if (!config.admin.components.beforeDashboard) {
290
+ // config.admin.components.beforeDashboard = []
291
+ // }
292
+ // config.admin.components.beforeDashboard.push(`payload-forge-ai/client#BeforeDashboardClient`)
293
+ // config.admin.components.beforeDashboard.push(`payload-forge-ai/rsc#BeforeDashboardServer`)
294
+ };
295
+ const hasName = (f)=>typeof f.name === 'string';
296
+ export function setAdminFieldComponent(field, fieldComponent) {
297
+ return {
298
+ ...field,
299
+ admin: {
300
+ ...field.admin ?? {},
301
+ components: {
302
+ ...field.admin?.components ?? {},
303
+ Field: fieldComponent
304
+ }
305
+ }
306
+ };
307
+ }
308
+
309
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Config, Field, PayloadRequest } from 'payload'\n\nimport { createOpenAI } from '@ai-sdk/openai'\nimport { generateText, type ModelMessage } from 'ai'\nimport { flattenTopLevelFields, InvalidConfiguration } from 'payload'\n\nexport type PayloadForgeAiContextField =\n | string\n | {\n field: string\n prompt?: string\n /**\n * For relationship fields, specify which fields to extract from the related document.\n * For other fields, specify which nested properties to extract.\n * If not specified, the entire value will be used.\n * Can be nested for complex object structures.\n */\n fields?: PayloadForgeAiContextField[]\n }\n\nexport type PayloadForgeAiFieldConfig = {\n context?: PayloadForgeAiContextField[]\n /**\n * If true, tells the AI to generate something different from the current value.\n * Defaults to true.\n */\n generateDifferent?: boolean\n name: string\n prompt: string\n}\n\nexport type PayloadForgeAiCollectionConfig = {\n fields: PayloadForgeAiFieldConfig[]\n}\n\nexport type PayloadForgeAiConfig = {\n apiKey: string\n collections?: Partial<Record<CollectionSlug, PayloadForgeAiCollectionConfig>>\n disabled?: boolean\n}\n\nfunction getValidFieldNames(collection: CollectionConfig): Set<string> {\n const flattened = flattenTopLevelFields(collection.fields)\n return new Set(flattened.map((f) => f.name).filter((name): name is string => name != null))\n}\n\nfunction validateCollectionFields(\n collectionSlug: string,\n collection: CollectionConfig,\n allowedFields: string[],\n): void {\n const validNames = getValidFieldNames(collection)\n const invalid = allowedFields.filter((name) => !validNames.has(name))\n if (invalid.length > 0) {\n throw new InvalidConfiguration(\n `payload-forge-ai: Collection \"${collectionSlug}\" does not have these fields: ${invalid.join(', ')}. ` +\n `Valid top-level fields: ${[...validNames].sort().join(', ')}.`,\n )\n }\n}\n\n/**\n * Recursively extract fields from an object based on field configuration\n */\nfunction extractFields(data: any, fieldConfigs: PayloadForgeAiContextField[]): Record<string, any> {\n if (!data || typeof data !== 'object') {\n return data\n }\n\n const result: Record<string, any> = {}\n\n for (const config of fieldConfigs) {\n const fieldName = typeof config === 'string' ? config : config.field\n const nestedFields = typeof config === 'string' ? undefined : config.fields\n\n if (data[fieldName] !== undefined) {\n if (nestedFields && nestedFields.length > 0) {\n // Recursively extract nested fields\n result[fieldName] = extractFields(data[fieldName], nestedFields)\n } else {\n // Just include the field value\n result[fieldName] = data[fieldName]\n }\n }\n }\n\n return result\n}\n\n/**\n * Recursively build context parts with prompts for nested fields\n */\nfunction buildContextParts(\n data: any,\n fieldConfigs: PayloadForgeAiContextField[],\n indent: string = ' ',\n): string[] {\n if (!data || typeof data !== 'object') {\n return []\n }\n\n const parts: string[] = []\n\n for (const config of fieldConfigs) {\n const fieldName = typeof config === 'string' ? config : config.field\n const customPrompt = typeof config === 'string' ? undefined : config.prompt\n const nestedFields = typeof config === 'string' ? undefined : config.fields\n const fieldValue = data[fieldName]\n\n if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {\n if (nestedFields && nestedFields.length > 0 && typeof fieldValue === 'object') {\n // Handle nested fields with their own prompts\n // Add the current field's prompt as a header if it exists\n if (customPrompt) {\n parts.push(`${indent}${customPrompt}:`)\n }\n const nestedParts = buildContextParts(fieldValue, nestedFields, indent + ' ')\n parts.push(...nestedParts)\n } else {\n // Leaf value - format with prompt\n let valueStr: string\n if (typeof fieldValue === 'object') {\n valueStr = JSON.stringify(fieldValue, null, 2)\n } else {\n valueStr = String(fieldValue)\n }\n\n if (customPrompt) {\n parts.push(`${indent}${customPrompt}: ${valueStr}`)\n } else {\n parts.push(`${indent}${fieldName}: ${valueStr}`)\n }\n }\n }\n }\n\n return parts\n}\n\nexport const payloadForgeAi =\n (pluginOptions: PayloadForgeAiConfig) =>\n (config: Config): Config => {\n if (!config.collections) {\n config.collections = []\n }\n\n if (pluginOptions.disabled) {\n return config\n }\n\n const openAi = createOpenAI({\n apiKey: pluginOptions.apiKey,\n })\n\n const model = openAi('gpt-5')\n\n const systemMessages: ModelMessage[] = [\n {\n content: `\n You are a helpful assistant that can help with the creation of content in payloadCMS fields. You must return only the content and not any other questions or comments!\n The content must be in the same language as the prompt.\n The content must be in the same format as the field type.\n The content must be in the same length as the field type.\n The content must be in the same style as the field type.\n The content must be in the same tone as the field type.\n The content must be in the same voice as the field type.\n You may vary the content to make it more interesting and engaging, but you must always return the content in the same language as the prompt.\n `,\n role: 'system',\n },\n ]\n const endpoints = config.endpoints ?? []\n\n if (pluginOptions.collections) {\n for (const collectionSlug in pluginOptions.collections) {\n const entry = pluginOptions.collections[collectionSlug]\n if (!entry?.fields?.length) {\n continue\n }\n\n const collection = config.collections.find(\n (c: CollectionConfig) => c.slug === collectionSlug,\n )\n if (!collection) {\n throw new InvalidConfiguration(\n `payload-forge-ai: Collection \"${collectionSlug}\" is not defined in config.collections.`,\n )\n }\n\n const fieldNames = entry.fields.map((f) => f.name)\n\n validateCollectionFields(collectionSlug, collection, fieldNames)\n\n entry.fields.forEach((field) => {\n // Validate context fields\n if (field.context && field.context.length > 0) {\n const contextFieldNames = field.context.map((ctx) =>\n typeof ctx === 'string' ? ctx : ctx.field,\n )\n validateCollectionFields(collectionSlug, collection, contextFieldNames)\n }\n\n endpoints.push({\n handler: async (req: PayloadRequest) => {\n const payload = req.payload\n const flattened = flattenTopLevelFields(collection.fields)\n\n const fieldData = flattened.find((f) => f.name === field.name)\n if (!fieldData) {\n throw new InvalidConfiguration(\n `payload-forge-ai: Collection \"${collectionSlug}\" does not have field \"${field.name}\".`,\n )\n }\n\n // Parse request body for context\n let contextData: Record<string, any> = {}\n let documentId: string | undefined\n let currentValue: any = undefined\n\n try {\n if (req.json) {\n const body = await req.json()\n contextData = body.context || {}\n documentId = body.documentId\n currentValue = body.currentValue\n }\n } catch (e) {\n // If no body or invalid JSON, continue without context\n }\n\n // Build context prompt from configured context fields\n let contextPrompt = ''\n if (field.context && field.context.length > 0) {\n const contextParts: string[] = []\n\n for (const ctx of field.context) {\n const fieldName = typeof ctx === 'string' ? ctx : ctx.field\n const customPrompt = typeof ctx === 'string' ? undefined : ctx.prompt\n const selectedFields = typeof ctx === 'string' ? undefined : ctx.fields\n let fieldValue = contextData[fieldName]\n\n if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {\n const contextField = flattened.find((f) => f.name === fieldName)\n const fieldType = contextField?.type || 'unknown'\n\n // Handle relationship fields - fetch the related document if we only have an ID\n if (contextField?.type === 'relationship' && typeof fieldValue === 'string') {\n try {\n const relationTo = (contextField as any).relationTo\n if (relationTo) {\n const relatedDoc = await payload.findByID({\n collection: relationTo,\n id: fieldValue,\n })\n fieldValue = relatedDoc\n }\n } catch (e) {\n console.error(`Failed to fetch related document for ${fieldName}:`, e)\n // Continue with the ID if fetch fails\n }\n }\n\n // Handle nested fields with recursive prompts\n if (\n selectedFields &&\n selectedFields.length > 0 &&\n typeof fieldValue === 'object'\n ) {\n // Add the outer prompt as a header if it exists\n if (customPrompt) {\n contextParts.push(`${customPrompt}:`)\n }\n console.log(`Building nested parts for field: ${fieldName}`)\n console.log(`Field value keys:`, Object.keys(fieldValue))\n console.log(\n `Selected fields config:`,\n JSON.stringify(selectedFields, null, 2),\n )\n const nestedParts = buildContextParts(fieldValue, selectedFields)\n console.log(`Nested parts result:`, nestedParts)\n contextParts.push(...nestedParts)\n } else {\n // Simple field without nesting\n let valueStr: string\n if (typeof fieldValue === 'object') {\n valueStr = JSON.stringify(fieldValue, null, 2)\n } else {\n valueStr = String(fieldValue)\n }\n\n if (customPrompt) {\n contextParts.push(`${customPrompt}: ${valueStr}`)\n } else {\n contextParts.push(`${fieldName} (${fieldType}): ${valueStr}`)\n }\n }\n }\n }\n\n if (contextParts.length > 0) {\n contextPrompt = '\\n\\nContext from other fields:\\n' + contextParts.join('\\n')\n }\n }\n\n // Add current value info if it exists\n let currentValuePrompt = ''\n const shouldGenerateDifferent = field.generateDifferent ?? true // Default to true\n if (currentValue !== undefined && currentValue !== null && currentValue !== '') {\n if (shouldGenerateDifferent) {\n currentValuePrompt = `\\n\\nCurrent value: ${currentValue}\\nIMPORTANT: Generate something DIFFERENT from the current value above. Do not repeat it.`\n } else {\n currentValuePrompt = `\\n\\nCurrent value: ${currentValue}`\n }\n }\n\n const messages: ModelMessage[] = [\n ...systemMessages,\n {\n content: `\nCollection: ${collectionSlug}\nField name: ${fieldData.name}\nField type: ${fieldData.type}\nCustom instructions: ${field.prompt}${contextPrompt}${currentValuePrompt}\n `,\n role: 'system',\n },\n ]\n\n console.log(messages[messages.length - 1].content)\n\n const response = await generateText({\n messages,\n model,\n })\n\n return Response.json(response.text)\n },\n method: 'post',\n path: `/forge-ai/${collectionSlug}/${field.name}`,\n })\n })\n }\n }\n\n const collectionSlugs = Object.keys(pluginOptions?.collections ?? {})\n\n return {\n ...config,\n collections: [\n ...(config.collections ?? []).filter(\n (c: CollectionConfig) => !collectionSlugs.includes(c.slug),\n ),\n ...collectionSlugs.map((collectionSlug) => {\n const collection = config.collections?.find(\n (c: CollectionConfig) => c.slug === collectionSlug,\n )\n\n if (!collection) {\n throw new InvalidConfiguration(\n `payload-forge-ai: Collection \"${collectionSlug}\" is not defined in config.collections.`,\n )\n }\n return {\n ...collection,\n fields: [\n ...collection.fields\n .filter((f: Field) => {\n if (!hasName(f)) {\n return true\n }\n return (\n pluginOptions.collections?.[collectionSlug]?.fields?.find(\n (field) => field.name === f.name,\n ) === undefined\n )\n })\n .map((f: Field) => {\n return f\n }),\n ...(pluginOptions.collections?.[collectionSlug]?.fields?.map((field) => {\n const fieldData = collection.fields\n .filter(hasName)\n .find((f: NamedField) => f.name === field.name) as Field | undefined\n\n if (!fieldData) {\n throw new InvalidConfiguration(\n `payload-forge-ai: Collection \"${collectionSlug}\" does not have field \"${field.name}\".`,\n )\n }\n\n return setAdminFieldComponent(\n fieldData,\n 'payload-forge-ai/ForgeAIField#ForgeAIField',\n )\n }) ?? []),\n ],\n }\n }),\n ],\n endpoints: [...(config.endpoints || []), ...endpoints],\n }\n\n // messages.push({\n // content:\n // 'You are a helpful assistant that can help with the creation of content for a website.',\n // role: 'system',\n // })\n\n // if (!config.admin) {\n // config.admin = {}\n // }\n\n // if (!config.admin.components) {\n // config.admin.components = {}\n // }\n\n // if (!config.admin.components.beforeDashboard) {\n // config.admin.components.beforeDashboard = []\n // }\n\n // config.admin.components.beforeDashboard.push(`payload-forge-ai/client#BeforeDashboardClient`)\n // config.admin.components.beforeDashboard.push(`payload-forge-ai/rsc#BeforeDashboardServer`)\n }\n\ntype NamedField = { name: string } & Field\n\nconst hasName = (f: Field): f is NamedField => typeof (f as any).name === 'string'\n\nexport function setAdminFieldComponent<T extends Field>(field: T, fieldComponent: string): T {\n return {\n ...field,\n admin: {\n ...(field.admin ?? {}),\n components: {\n ...(field.admin?.components ?? {}),\n Field: fieldComponent,\n },\n } as T['admin'], // <- critical: prevents Admin.position widening to string\n }\n}\n"],"names":["createOpenAI","generateText","flattenTopLevelFields","InvalidConfiguration","getValidFieldNames","collection","flattened","fields","Set","map","f","name","filter","validateCollectionFields","collectionSlug","allowedFields","validNames","invalid","has","length","join","sort","extractFields","data","fieldConfigs","result","config","fieldName","field","nestedFields","undefined","buildContextParts","indent","parts","customPrompt","prompt","fieldValue","push","nestedParts","valueStr","JSON","stringify","String","payloadForgeAi","pluginOptions","collections","disabled","openAi","apiKey","model","systemMessages","content","role","endpoints","entry","find","c","slug","fieldNames","forEach","context","contextFieldNames","ctx","handler","req","payload","fieldData","contextData","documentId","currentValue","json","body","e","contextPrompt","contextParts","selectedFields","contextField","fieldType","type","relationTo","relatedDoc","findByID","id","console","error","log","Object","keys","currentValuePrompt","shouldGenerateDifferent","generateDifferent","messages","response","Response","text","method","path","collectionSlugs","includes","hasName","setAdminFieldComponent","fieldComponent","admin","components","Field"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "payload-forgeai",
3
+ "version": "0.2.0",
4
+ "description": "A plugin for Payload CMS to generate content with AI",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
21
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
22
+ "build:types": "tsc --outDir dist --rootDir ./src",
23
+ "clean": "rimraf {dist,*.tsbuildinfo}",
24
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
25
+ "dev": "next dev dev --turbo",
26
+ "dev:generate-importmap": "pnpm dev:payload generate:importmap",
27
+ "dev:generate-types": "pnpm dev:payload generate:types",
28
+ "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
29
+ "generate:importmap": "pnpm dev:generate-importmap",
30
+ "generate:types": "pnpm dev:generate-types",
31
+ "lint": "eslint",
32
+ "lint:fix": "eslint ./src --fix",
33
+ "test": "pnpm test:int && pnpm test:e2e",
34
+ "test:e2e": "playwright test",
35
+ "test:int": "vitest"
36
+ },
37
+ "devDependencies": {
38
+ "@eslint/eslintrc": "^3.2.0",
39
+ "@payloadcms/db-mongodb": "3.69.0",
40
+ "@payloadcms/db-postgres": "3.69.0",
41
+ "@payloadcms/db-sqlite": "3.69.0",
42
+ "@payloadcms/eslint-config": "3.9.0",
43
+ "@payloadcms/next": "3.69.0",
44
+ "@payloadcms/richtext-lexical": "3.69.0",
45
+ "@payloadcms/ui": "3.69.0",
46
+ "@playwright/test": "1.56.1",
47
+ "@swc-node/register": "1.10.9",
48
+ "@swc/cli": "0.6.0",
49
+ "@types/node": "^22.5.4",
50
+ "@types/react": "19.2.9",
51
+ "@types/react-dom": "19.2.3",
52
+ "copyfiles": "2.4.1",
53
+ "cross-env": "^7.0.3",
54
+ "eslint": "^9.23.0",
55
+ "eslint-config-next": "15.4.11",
56
+ "graphql": "^16.8.1",
57
+ "mongodb-memory-server": "10.1.4",
58
+ "next": "16.1.6",
59
+ "open": "^10.1.0",
60
+ "payload": "3.69.0",
61
+ "prettier": "^3.4.2",
62
+ "qs-esm": "7.0.2",
63
+ "react": "19.2.1",
64
+ "react-dom": "19.2.1",
65
+ "rimraf": "3.0.2",
66
+ "sharp": "0.34.2",
67
+ "sort-package-json": "^2.10.0",
68
+ "typescript": "5.7.3",
69
+ "vite-tsconfig-paths": "^5.1.4",
70
+ "vitest": "^3.1.2"
71
+ },
72
+ "peerDependencies": {
73
+ "payload": "^3.69.0"
74
+ },
75
+ "engines": {
76
+ "node": ">=20.9.0",
77
+ "pnpm": "^9 || ^10"
78
+ },
79
+ "publishConfig": {
80
+ "exports": {
81
+ ".": {
82
+ "import": "./dist/index.js",
83
+ "types": "./dist/index.d.ts",
84
+ "default": "./dist/index.js"
85
+ },
86
+ "./ForgeAIField": {
87
+ "import": "./dist/exports/ForgeAIField.js",
88
+ "types": "./dist/exports/ForgeAIField.ts",
89
+ "default": "./dist/exports/ForgeAIField.js"
90
+ }
91
+ },
92
+ "main": "./dist/index.js",
93
+ "types": "./dist/index.d.ts"
94
+ },
95
+ "pnpm": {
96
+ "onlyBuiltDependencies": [
97
+ "sharp",
98
+ "esbuild",
99
+ "unrs-resolver"
100
+ ]
101
+ },
102
+ "registry": "https://registry.npmjs.org/",
103
+ "dependencies": {
104
+ "@ai-sdk/openai": "^3.0.25",
105
+ "ai": "^6.0.71"
106
+ }
107
+ }