@karixi/payload-ai 0.1.2 → 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 +112 -4
- package/dist/base-CWybHD_y.mjs +313 -0
- package/dist/base-CWybHD_y.mjs.map +1 -0
- package/dist/content-generator-BcUxGqga.mjs +609 -0
- package/dist/content-generator-BcUxGqga.mjs.map +1 -0
- package/dist/index.d.mts +248 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +439 -51
- package/dist/index.mjs.map +1 -1
- package/dist/{schema-reader-ZkoI6Pwi.mjs → schema-reader-N3-mDRkR.mjs} +72 -15
- package/dist/schema-reader-N3-mDRkR.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/base-BPwZbeh1.mjs +0 -146
- package/dist/base-BPwZbeh1.mjs.map +0 -1
- package/dist/content-generator-fPX2DL3g.mjs +0 -220
- package/dist/content-generator-fPX2DL3g.mjs.map +0 -1
- package/dist/schema-reader-ZkoI6Pwi.mjs.map +0 -1
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
+
//#region src/generate/block-generator.ts
|
|
3
|
+
/**
|
|
4
|
+
* Block generation: turn a Blocks field definition into a JSON-Schema
|
|
5
|
+
* discriminated union + prompt fragment, and validate generated values
|
|
6
|
+
* back into shape.
|
|
7
|
+
*
|
|
8
|
+
* Blocks were previously marked SKIP across the pipeline. This module
|
|
9
|
+
* lets prompt-builder + content-generator include them without forcing
|
|
10
|
+
* any refactor of the existing switch-case: the top-level modules simply
|
|
11
|
+
* call into this file when they see `field.type === 'blocks'`.
|
|
12
|
+
*/
|
|
13
|
+
/** JSON-Schema fragment describing the allowed shape of one blocks field. */
|
|
14
|
+
function blocksOutputSchema(field) {
|
|
15
|
+
const blocks = field.blocks ?? [];
|
|
16
|
+
if (blocks.length === 0) return {
|
|
17
|
+
type: "array",
|
|
18
|
+
items: {}
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: {
|
|
23
|
+
oneOf: blocks.map((block) => ({
|
|
24
|
+
type: "object",
|
|
25
|
+
required: ["blockType"],
|
|
26
|
+
properties: {
|
|
27
|
+
blockType: {
|
|
28
|
+
type: "string",
|
|
29
|
+
const: block.slug
|
|
30
|
+
},
|
|
31
|
+
...blockFieldProperties(block)
|
|
32
|
+
}
|
|
33
|
+
})),
|
|
34
|
+
discriminator: { propertyName: "blockType" }
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function blockFieldProperties(block) {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const f of block.fields) {
|
|
41
|
+
if (f.type === "relationship" || f.type === "upload") continue;
|
|
42
|
+
if (f.type === "richText") {
|
|
43
|
+
out[f.name] = { type: "string" };
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (f.type === "blocks") {
|
|
47
|
+
out[f.name] = blocksOutputSchema(f);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (f.type === "array") {
|
|
51
|
+
out[f.name] = {
|
|
52
|
+
type: "array",
|
|
53
|
+
items: { type: "object" }
|
|
54
|
+
};
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (f.type === "group") {
|
|
58
|
+
out[f.name] = { type: "object" };
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (f.type === "select" || f.type === "radio") {
|
|
62
|
+
const values = (f.options ?? []).map((o) => o.value);
|
|
63
|
+
out[f.name] = {
|
|
64
|
+
type: "string",
|
|
65
|
+
enum: values
|
|
66
|
+
};
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (f.type === "number") {
|
|
70
|
+
out[f.name] = { type: "number" };
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (f.type === "checkbox") {
|
|
74
|
+
out[f.name] = { type: "boolean" };
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (f.type === "date") {
|
|
78
|
+
out[f.name] = {
|
|
79
|
+
type: "string",
|
|
80
|
+
format: "date-time"
|
|
81
|
+
};
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
out[f.name] = { type: "string" };
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** Prompt fragment describing a blocks field and its available block catalog. */
|
|
89
|
+
function describeBlocksField(field) {
|
|
90
|
+
const blocks = field.blocks ?? [];
|
|
91
|
+
if (blocks.length === 0) return `- "${field.name}" (blocks${field.required ? " (required)" : " (optional)"}): empty block catalog — omit`;
|
|
92
|
+
const lines = [`- "${field.name}" (blocks${field.required ? " (required)" : " (optional)"}): array of block objects. Each object MUST include a "blockType" discriminator string.`, " Available block types:"];
|
|
93
|
+
for (const block of blocks) {
|
|
94
|
+
const label = block.label ? ` (${block.label})` : "";
|
|
95
|
+
const desc = block.description ? ` — ${block.description}` : "";
|
|
96
|
+
lines.push(` • blockType: "${block.slug}"${label}${desc}`);
|
|
97
|
+
for (const f of block.fields) {
|
|
98
|
+
const required = f.required ? " (required)" : "";
|
|
99
|
+
const suffix = describeBlockFieldInline(f);
|
|
100
|
+
lines.push(` - ${f.name} (${f.type}${required})${suffix}`);
|
|
101
|
+
}
|
|
102
|
+
if (block.requiredFields && block.requiredFields.length > 0) lines.push(` required: ${block.requiredFields.join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push(` Generate 2–5 blocks forming a coherent layout; mix block types if the theme permits.`);
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
function describeBlockFieldInline(f) {
|
|
108
|
+
if (f.type === "select" || f.type === "radio") {
|
|
109
|
+
const values = (f.options ?? []).map((o) => `"${o.value}"`).join(", ");
|
|
110
|
+
return values ? ` — one of [${values}]` : "";
|
|
111
|
+
}
|
|
112
|
+
if (f.type === "relationship") return " — use an existing ID from related collections";
|
|
113
|
+
if (f.type === "upload") return " — omit; media is attached separately";
|
|
114
|
+
if (f.type === "richText") return " — plain text paragraph";
|
|
115
|
+
if (f.type === "number") return " — numeric value";
|
|
116
|
+
if (f.type === "checkbox") return " — boolean";
|
|
117
|
+
if (f.type === "date") return " — ISO 8601 timestamp";
|
|
118
|
+
if (f.type === "blocks") return " — nested block array";
|
|
119
|
+
if (f.type === "array") return " — array of sub-objects";
|
|
120
|
+
if (f.type === "group") return " — object of sub-fields";
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Validate a generated blocks array against the schema. Drops blocks with
|
|
125
|
+
* unknown blockType; reports missing required fields per block.
|
|
126
|
+
* Returns { valid, issues } — `valid` is a cleaned array safe to persist.
|
|
127
|
+
*/
|
|
128
|
+
function validateBlocks(value, field) {
|
|
129
|
+
const issues = [];
|
|
130
|
+
if (!Array.isArray(value)) {
|
|
131
|
+
if (value === void 0 || value === null) return {
|
|
132
|
+
valid: [],
|
|
133
|
+
issues
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
valid: [],
|
|
137
|
+
issues: [{
|
|
138
|
+
path: field.name,
|
|
139
|
+
message: "expected array of blocks"
|
|
140
|
+
}]
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const catalog = new Map((field.blocks ?? []).map((b) => [b.slug, b]));
|
|
144
|
+
const valid = [];
|
|
145
|
+
value.forEach((item, idx) => {
|
|
146
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
147
|
+
issues.push({
|
|
148
|
+
path: `${field.name}[${idx}]`,
|
|
149
|
+
message: "block must be an object"
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const obj = item;
|
|
154
|
+
const blockType = obj.blockType;
|
|
155
|
+
if (typeof blockType !== "string") {
|
|
156
|
+
issues.push({
|
|
157
|
+
path: `${field.name}[${idx}]`,
|
|
158
|
+
message: "missing blockType discriminator"
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const block = catalog.get(blockType);
|
|
163
|
+
if (!block) {
|
|
164
|
+
issues.push({
|
|
165
|
+
path: `${field.name}[${idx}]`,
|
|
166
|
+
message: `unknown blockType "${blockType}" — valid: ${[...catalog.keys()].join(", ")}`
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const missing = (block.requiredFields ?? []).filter((name) => {
|
|
171
|
+
const v = obj[name];
|
|
172
|
+
return v === void 0 || v === null || v === "";
|
|
173
|
+
});
|
|
174
|
+
if (missing.length > 0) {
|
|
175
|
+
issues.push({
|
|
176
|
+
path: `${field.name}[${idx}]`,
|
|
177
|
+
message: `${blockType} missing required: ${missing.join(", ")}`
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
valid.push(obj);
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
valid,
|
|
185
|
+
issues
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/generate/richtext-generator.ts
|
|
190
|
+
function makeTextNode(text, format = 0) {
|
|
191
|
+
return {
|
|
192
|
+
detail: 0,
|
|
193
|
+
format,
|
|
194
|
+
mode: "normal",
|
|
195
|
+
style: "",
|
|
196
|
+
text,
|
|
197
|
+
type: "text",
|
|
198
|
+
version: 1
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function makeParagraph(text) {
|
|
202
|
+
return {
|
|
203
|
+
children: text.length > 0 ? [makeTextNode(text)] : [],
|
|
204
|
+
direction: "ltr",
|
|
205
|
+
format: "",
|
|
206
|
+
indent: 0,
|
|
207
|
+
type: "paragraph",
|
|
208
|
+
version: 1
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function makeHeading(text, tag = "h2") {
|
|
212
|
+
return {
|
|
213
|
+
children: [makeTextNode(text)],
|
|
214
|
+
direction: "ltr",
|
|
215
|
+
format: "",
|
|
216
|
+
indent: 0,
|
|
217
|
+
type: "heading",
|
|
218
|
+
tag,
|
|
219
|
+
version: 1
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function makeList(items, listType = "bullet") {
|
|
223
|
+
return {
|
|
224
|
+
children: items.map((item, index) => ({
|
|
225
|
+
children: [makeTextNode(item)],
|
|
226
|
+
direction: "ltr",
|
|
227
|
+
format: "",
|
|
228
|
+
indent: 0,
|
|
229
|
+
type: "listitem",
|
|
230
|
+
value: index + 1,
|
|
231
|
+
version: 1
|
|
232
|
+
})),
|
|
233
|
+
direction: "ltr",
|
|
234
|
+
format: "",
|
|
235
|
+
indent: 0,
|
|
236
|
+
type: "list",
|
|
237
|
+
listType,
|
|
238
|
+
tag: listType === "bullet" ? "ul" : "ol",
|
|
239
|
+
version: 1
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Convert plain text to a Lexical root node.
|
|
244
|
+
* Each non-empty line becomes a paragraph.
|
|
245
|
+
*/
|
|
246
|
+
function textToLexical(text) {
|
|
247
|
+
return { root: {
|
|
248
|
+
children: text.split("\n").map((line) => makeParagraph(line)),
|
|
249
|
+
direction: "ltr",
|
|
250
|
+
format: "",
|
|
251
|
+
indent: 0,
|
|
252
|
+
type: "root",
|
|
253
|
+
version: 1
|
|
254
|
+
} };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Convert structured content to a Lexical root node.
|
|
258
|
+
* Sections may have an optional heading, paragraphs, and bullet points.
|
|
259
|
+
*/
|
|
260
|
+
function contentToLexical(content) {
|
|
261
|
+
const children = [];
|
|
262
|
+
for (const section of content.sections) {
|
|
263
|
+
if (section.heading) children.push(makeHeading(section.heading, "h2"));
|
|
264
|
+
for (const paragraph of section.paragraphs) children.push(makeParagraph(paragraph));
|
|
265
|
+
if (section.bulletPoints && section.bulletPoints.length > 0) children.push(makeList(section.bulletPoints, "bullet"));
|
|
266
|
+
}
|
|
267
|
+
return { root: {
|
|
268
|
+
children,
|
|
269
|
+
direction: "ltr",
|
|
270
|
+
format: "",
|
|
271
|
+
indent: 0,
|
|
272
|
+
type: "root",
|
|
273
|
+
version: 1
|
|
274
|
+
} };
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/core/prompt-builder.ts
|
|
278
|
+
function detectHeadingLevels(features) {
|
|
279
|
+
const levels = /* @__PURE__ */ new Set();
|
|
280
|
+
for (const f of features) {
|
|
281
|
+
const m = f.match(/h([1-6])/i);
|
|
282
|
+
if (m) levels.add(Number.parseInt(m[1], 10));
|
|
283
|
+
}
|
|
284
|
+
if (features.some((f) => /heading/i.test(f) && !/h[1-6]/i.test(f))) {
|
|
285
|
+
levels.add(2);
|
|
286
|
+
levels.add(3);
|
|
287
|
+
levels.add(4);
|
|
288
|
+
}
|
|
289
|
+
return [...levels].sort((a, b) => a - b);
|
|
290
|
+
}
|
|
291
|
+
function describeField(field, existingIds, includeBlocks = false, adapters, domain) {
|
|
292
|
+
const lines = [];
|
|
293
|
+
const required = field.required ? " (required)" : " (optional)";
|
|
294
|
+
switch (field.type) {
|
|
295
|
+
case "text":
|
|
296
|
+
case "textarea":
|
|
297
|
+
case "email":
|
|
298
|
+
lines.push(`- "${field.name}" (${field.type}${required}): Generate realistic ${field.type} content`);
|
|
299
|
+
break;
|
|
300
|
+
case "number":
|
|
301
|
+
lines.push(`- "${field.name}" (number${required}): Generate a realistic numeric value (e.g. price 1–999, quantity 1–100, rating 1–5)`);
|
|
302
|
+
break;
|
|
303
|
+
case "checkbox":
|
|
304
|
+
lines.push(`- "${field.name}" (boolean${required}): true or false`);
|
|
305
|
+
break;
|
|
306
|
+
case "date":
|
|
307
|
+
lines.push(`- "${field.name}" (date${required}): ISO 8601 date string (e.g. "2024-06-15T10:00:00.000Z")`);
|
|
308
|
+
break;
|
|
309
|
+
case "select": {
|
|
310
|
+
const values = (field.options ?? []).map((o) => `"${o.value}"`).join(", ");
|
|
311
|
+
lines.push(`- "${field.name}" (select${required}): Must be one of [${values}]`);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "relationship": {
|
|
315
|
+
const collections = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo ?? ""];
|
|
316
|
+
const idLists = collections.map((col) => {
|
|
317
|
+
const ids = existingIds?.[col] ?? [];
|
|
318
|
+
return ids.length > 0 ? `${col}: [${ids.map((id) => `"${id}"`).join(", ")}]` : `${col}: (no existing IDs available — omit this field)`;
|
|
319
|
+
});
|
|
320
|
+
if (collections.some((col) => (existingIds?.[col] ?? []).length > 0)) lines.push(`- "${field.name}" (relationship${required}): Pick from existing IDs — ${idLists.join("; ")}${field.hasMany ? " (can be an array of IDs)" : " (single ID string)"}`);
|
|
321
|
+
else lines.push(`- "${field.name}" (relationship${required}): SKIP — no existing IDs available`);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case "richText": {
|
|
325
|
+
const features = field.lexicalFeatures ?? [];
|
|
326
|
+
if (features.length > 0) {
|
|
327
|
+
const headingLevels = detectHeadingLevels(features);
|
|
328
|
+
const allowLists = features.some((f) => /list|bullet|number|ordered/i.test(f));
|
|
329
|
+
const capLines = [];
|
|
330
|
+
if (headingLevels.length > 0) capLines.push(`headings (${headingLevels.map((l) => `h${l}`).join(", ")})`);
|
|
331
|
+
if (allowLists) capLines.push("bullet/numbered lists");
|
|
332
|
+
capLines.push("paragraphs");
|
|
333
|
+
lines.push(`- "${field.name}" (richtext${required}): Return a structured object {"sections":[{"heading":"optional","paragraphs":["..."],"bulletPoints":["..."]}]}. The editor supports: ${capLines.join(", ")}. Keep each section short (1–3 paragraphs).`);
|
|
334
|
+
} else lines.push(`- "${field.name}" (richtext${required}): Return PLAIN TEXT only (do not wrap in Lexical/JSON — the system will convert it). Write 1–3 sentences of realistic content.`);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case "upload":
|
|
338
|
+
lines.push(`- "${field.name}" (upload${required}): SKIP — handled separately`);
|
|
339
|
+
break;
|
|
340
|
+
case "array":
|
|
341
|
+
case "group":
|
|
342
|
+
if (field.fields && field.fields.length > 0) {
|
|
343
|
+
lines.push(`- "${field.name}" (${field.type}${required}):`);
|
|
344
|
+
for (const subField of field.fields) lines.push(` ${describeField(subField, existingIds, includeBlocks, adapters, domain)}`);
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
case "blocks":
|
|
348
|
+
if (includeBlocks) lines.push(describeBlocksField(field));
|
|
349
|
+
else lines.push(`- "${field.name}" (blocks${required}): SKIP — complex layout field, omit`);
|
|
350
|
+
break;
|
|
351
|
+
default: {
|
|
352
|
+
const adapter = adapters?.get(field.type);
|
|
353
|
+
if (adapter?.describe) {
|
|
354
|
+
const fragment = adapter.describe(field, {
|
|
355
|
+
existingIds,
|
|
356
|
+
domain
|
|
357
|
+
});
|
|
358
|
+
if (fragment !== null) lines.push(fragment);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
lines.push(`- "${field.name}" (${field.type}${required}): Generate appropriate content`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return lines.join("\n");
|
|
365
|
+
}
|
|
366
|
+
function buildGenerationPrompt(schema, context) {
|
|
367
|
+
const fieldDescriptions = schema.fields.map((f) => describeField(f, context.existingIds, context.includeBlocks ?? false, context.adapters, context.domain)).join("\n");
|
|
368
|
+
const requiredNote = schema.requiredFields.length > 0 ? `\nRequired fields (must be present): ${schema.requiredFields.map((f) => `"${f}"`).join(", ")}` : "";
|
|
369
|
+
const themeNote = context.theme ? `\nContent theme/style: ${context.theme}` : "";
|
|
370
|
+
const localeNote = context.locale && context.locale !== "en" ? `\nGenerate content in locale: ${context.locale}` : "";
|
|
371
|
+
const domain = context.domain === void 0 ? "an ecommerce platform" : context.domain.trim().length === 0 ? "" : context.domain;
|
|
372
|
+
const domainNote = domain ? ` appropriate for ${domain}` : "";
|
|
373
|
+
return `Generate ${context.count} realistic document(s) for the "${schema.slug}" collection.
|
|
374
|
+
${themeNote}${localeNote}${requiredNote}
|
|
375
|
+
|
|
376
|
+
Fields to generate:
|
|
377
|
+
${fieldDescriptions}
|
|
378
|
+
|
|
379
|
+
Rules:
|
|
380
|
+
- Return a JSON array with exactly ${context.count} item(s)
|
|
381
|
+
- Each item must be a flat JSON object with field names as keys
|
|
382
|
+
- Skip fields marked as SKIP
|
|
383
|
+
- For richtext fields: return plain text strings only
|
|
384
|
+
- For relationship fields: use the provided existing IDs exactly as shown
|
|
385
|
+
- Do not include extra fields not listed above
|
|
386
|
+
- Generate varied, realistic content${domainNote}`;
|
|
387
|
+
}
|
|
388
|
+
function buildOutputSchema(schema, options) {
|
|
389
|
+
const properties = {};
|
|
390
|
+
const required = [];
|
|
391
|
+
const includeBlocks = options?.includeBlocks === true;
|
|
392
|
+
const adapters = options?.adapters;
|
|
393
|
+
for (const field of schema.fields) {
|
|
394
|
+
if (field.type === "blocks") {
|
|
395
|
+
if (!includeBlocks) continue;
|
|
396
|
+
properties[field.name] = blocksOutputSchema(field);
|
|
397
|
+
if (field.required) required.push(field.name);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if ([
|
|
401
|
+
"relationship",
|
|
402
|
+
"richText",
|
|
403
|
+
"upload"
|
|
404
|
+
].includes(field.type)) continue;
|
|
405
|
+
let fieldSchema;
|
|
406
|
+
switch (field.type) {
|
|
407
|
+
case "text":
|
|
408
|
+
case "textarea":
|
|
409
|
+
case "email":
|
|
410
|
+
case "date":
|
|
411
|
+
fieldSchema = { type: "string" };
|
|
412
|
+
break;
|
|
413
|
+
case "number":
|
|
414
|
+
fieldSchema = { type: "number" };
|
|
415
|
+
break;
|
|
416
|
+
case "checkbox":
|
|
417
|
+
fieldSchema = { type: "boolean" };
|
|
418
|
+
break;
|
|
419
|
+
case "select":
|
|
420
|
+
fieldSchema = {
|
|
421
|
+
type: "string",
|
|
422
|
+
enum: (field.options ?? []).map((o) => o.value)
|
|
423
|
+
};
|
|
424
|
+
break;
|
|
425
|
+
case "array":
|
|
426
|
+
fieldSchema = {
|
|
427
|
+
type: "array",
|
|
428
|
+
items: { type: "object" }
|
|
429
|
+
};
|
|
430
|
+
break;
|
|
431
|
+
case "group":
|
|
432
|
+
fieldSchema = { type: "object" };
|
|
433
|
+
break;
|
|
434
|
+
default: {
|
|
435
|
+
const custom = (adapters?.get(field.type))?.outputSchema?.(field);
|
|
436
|
+
if (custom !== void 0) {
|
|
437
|
+
if (custom === null) continue;
|
|
438
|
+
fieldSchema = custom;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
fieldSchema = { type: "string" };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
properties[field.name] = fieldSchema;
|
|
445
|
+
if (field.required) required.push(field.name);
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
type: "object",
|
|
449
|
+
properties: { items: {
|
|
450
|
+
type: "array",
|
|
451
|
+
items: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties,
|
|
454
|
+
required
|
|
455
|
+
}
|
|
456
|
+
} }
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/core/content-generator.ts
|
|
461
|
+
var content_generator_exports = /* @__PURE__ */ __exportAll({ generateDocuments: () => generateDocuments });
|
|
462
|
+
function getSelectValues(field) {
|
|
463
|
+
return (field.options ?? []).map((o) => o.value);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Convert richText values (strings or {sections:[...]} objects) into Lexical
|
|
467
|
+
* JSON. Other values pass through unchanged. This is defensive — if the AI
|
|
468
|
+
* returns something already shaped like Lexical (has `root`), we keep it.
|
|
469
|
+
*/
|
|
470
|
+
function convertRichTextValue(value) {
|
|
471
|
+
if (value === null || value === void 0) return value;
|
|
472
|
+
if (typeof value === "string") return textToLexical(value);
|
|
473
|
+
if (typeof value === "object") {
|
|
474
|
+
const v = value;
|
|
475
|
+
if (v.root !== void 0) return v;
|
|
476
|
+
if (Array.isArray(v.sections)) return contentToLexical(v);
|
|
477
|
+
}
|
|
478
|
+
return value;
|
|
479
|
+
}
|
|
480
|
+
/** Walk the generated doc and convert every richText field to Lexical. */
|
|
481
|
+
function applyRichTextPostprocess(doc, fields) {
|
|
482
|
+
for (const field of fields) {
|
|
483
|
+
if (!(field.name in doc)) continue;
|
|
484
|
+
if (field.type === "richText") {
|
|
485
|
+
doc[field.name] = convertRichTextValue(doc[field.name]);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (field.type === "group" && field.fields && typeof doc[field.name] === "object") {
|
|
489
|
+
const sub = doc[field.name];
|
|
490
|
+
if (sub && !Array.isArray(sub)) applyRichTextPostprocess(sub, field.fields);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (field.type === "array" && field.fields && Array.isArray(doc[field.name])) {
|
|
494
|
+
for (const item of doc[field.name]) if (item && typeof item === "object" && !Array.isArray(item)) applyRichTextPostprocess(item, field.fields);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return doc;
|
|
498
|
+
}
|
|
499
|
+
function validateDocument(doc, schema, options = {}) {
|
|
500
|
+
const errors = [];
|
|
501
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return [{
|
|
502
|
+
field: "_root",
|
|
503
|
+
message: "Document must be a plain object"
|
|
504
|
+
}];
|
|
505
|
+
const record = doc;
|
|
506
|
+
for (const fieldName of schema.requiredFields) {
|
|
507
|
+
const field = schema.fields.find((f) => f.name === fieldName);
|
|
508
|
+
if (!field) continue;
|
|
509
|
+
if ([
|
|
510
|
+
"relationship",
|
|
511
|
+
"richText",
|
|
512
|
+
"upload"
|
|
513
|
+
].includes(field.type)) continue;
|
|
514
|
+
if (field.type === "blocks" && !options.includeBlocks) continue;
|
|
515
|
+
if (record[fieldName] === void 0 || record[fieldName] === null || record[fieldName] === "") errors.push({
|
|
516
|
+
field: fieldName,
|
|
517
|
+
message: `Required field "${fieldName}" is missing or empty`
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
for (const field of schema.fields) {
|
|
521
|
+
if (field.type !== "select") continue;
|
|
522
|
+
const value = record[field.name];
|
|
523
|
+
if (value === void 0 || value === null) continue;
|
|
524
|
+
const validValues = getSelectValues(field);
|
|
525
|
+
if (validValues.length > 0 && !validValues.includes(String(value))) errors.push({
|
|
526
|
+
field: field.name,
|
|
527
|
+
message: `Field "${field.name}" has invalid select value "${String(value)}". Must be one of: ${validValues.join(", ")}`
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
if (options.includeBlocks) for (const field of schema.fields) {
|
|
531
|
+
if (field.type !== "blocks") continue;
|
|
532
|
+
const value = record[field.name];
|
|
533
|
+
if (value === void 0) continue;
|
|
534
|
+
const { valid, issues } = validateBlocks(value, field);
|
|
535
|
+
record[field.name] = valid;
|
|
536
|
+
for (const issue of issues) errors.push({
|
|
537
|
+
field: issue.path,
|
|
538
|
+
message: issue.message
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (options.adapters) for (const field of schema.fields) {
|
|
542
|
+
const adapter = options.adapters.get(field.type);
|
|
543
|
+
if (!adapter?.validate) continue;
|
|
544
|
+
const value = record[field.name];
|
|
545
|
+
if (value === void 0) continue;
|
|
546
|
+
const adapterIssues = adapter.validate(value, field);
|
|
547
|
+
for (const issue of adapterIssues) errors.push({
|
|
548
|
+
field: issue.field,
|
|
549
|
+
message: issue.message
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
return errors;
|
|
553
|
+
}
|
|
554
|
+
function buildRetryPrompt(originalPrompt, errors) {
|
|
555
|
+
return `${originalPrompt}
|
|
556
|
+
|
|
557
|
+
IMPORTANT: Your previous response had validation errors. Fix these issues:
|
|
558
|
+
${errors.map((e) => `- ${e.field}: ${e.message}`).join("\n")}
|
|
559
|
+
|
|
560
|
+
Return a corrected JSON array addressing all the errors above.`;
|
|
561
|
+
}
|
|
562
|
+
async function generateDocuments(provider, schema, context, options) {
|
|
563
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
564
|
+
const outputSchema = buildOutputSchema(schema, {
|
|
565
|
+
includeBlocks: context.includeBlocks === true,
|
|
566
|
+
adapters: context.adapters
|
|
567
|
+
});
|
|
568
|
+
let prompt = buildGenerationPrompt(schema, context);
|
|
569
|
+
let lastError = null;
|
|
570
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
571
|
+
let rawItems;
|
|
572
|
+
try {
|
|
573
|
+
rawItems = await provider.generate(prompt, outputSchema);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
576
|
+
if (attempt < maxRetries) {
|
|
577
|
+
prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), [{
|
|
578
|
+
field: "_generation",
|
|
579
|
+
message: `API error: ${lastError.message}`
|
|
580
|
+
}]);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
throw lastError;
|
|
584
|
+
}
|
|
585
|
+
const allErrors = [];
|
|
586
|
+
const validDocuments = [];
|
|
587
|
+
for (const item of rawItems) {
|
|
588
|
+
const errors = validateDocument(item, schema, {
|
|
589
|
+
includeBlocks: context.includeBlocks === true,
|
|
590
|
+
adapters: context.adapters
|
|
591
|
+
});
|
|
592
|
+
if (errors.length > 0) allErrors.push(...errors);
|
|
593
|
+
else validDocuments.push(item);
|
|
594
|
+
}
|
|
595
|
+
if (allErrors.length === 0) return { documents: validDocuments.map((d) => applyRichTextPostprocess(d, schema.fields)) };
|
|
596
|
+
if (attempt < maxRetries) {
|
|
597
|
+
prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), allErrors);
|
|
598
|
+
lastError = /* @__PURE__ */ new Error(`Validation failed: ${allErrors.map((e) => e.message).join("; ")}`);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (validDocuments.length > 0) return { documents: validDocuments.map((d) => applyRichTextPostprocess(d, schema.fields)) };
|
|
602
|
+
throw new Error(`Content generation failed after ${maxRetries} retries. Last errors: ${allErrors.map((e) => e.message).join("; ")}`);
|
|
603
|
+
}
|
|
604
|
+
throw lastError ?? /* @__PURE__ */ new Error("Content generation failed");
|
|
605
|
+
}
|
|
606
|
+
//#endregion
|
|
607
|
+
export { validateBlocks as a, describeBlocksField as i, generateDocuments as n, blocksOutputSchema as r, content_generator_exports as t };
|
|
608
|
+
|
|
609
|
+
//# sourceMappingURL=content-generator-BcUxGqga.mjs.map
|