@paroicms/site-generator-plugin 0.1.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 (59) hide show
  1. package/README.md +9 -0
  2. package/gen-backend/dist/context.js +2 -0
  3. package/gen-backend/dist/data-format.js +37 -0
  4. package/gen-backend/dist/generator/actions.js +35 -0
  5. package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js +227 -0
  6. package/gen-backend/dist/generator/fake-content-generator.ts/create-node-contents.js +156 -0
  7. package/gen-backend/dist/generator/fake-content-generator.ts/fake-content-types.js +1 -0
  8. package/gen-backend/dist/generator/fake-content-generator.ts/generate-fake-content.js +127 -0
  9. package/gen-backend/dist/generator/fake-content-generator.ts/invoke-generate-fake-content.js +49 -0
  10. package/gen-backend/dist/generator/generator-types.js +1 -0
  11. package/gen-backend/dist/generator/helpers/esm-module.helper.js +6 -0
  12. package/gen-backend/dist/generator/helpers/js-utils.js +14 -0
  13. package/gen-backend/dist/generator/lib/common-types.js +1 -0
  14. package/gen-backend/dist/generator/lib/create-prompt.js +44 -0
  15. package/gen-backend/dist/generator/lib/debug-utils.js +118 -0
  16. package/gen-backend/dist/generator/lib/images-lib.js +16 -0
  17. package/gen-backend/dist/generator/lib/llm-invoke-types.js +1 -0
  18. package/gen-backend/dist/generator/lib/llm-tokens.js +10 -0
  19. package/gen-backend/dist/generator/lib/markdown-bulleted-list-parser.js +147 -0
  20. package/gen-backend/dist/generator/lib/parse-llm-response.js +160 -0
  21. package/gen-backend/dist/generator/lib/tasks.js +112 -0
  22. package/gen-backend/dist/generator/lib/utils.js +13 -0
  23. package/gen-backend/dist/generator/llm-queries/invoke-message-guard.js +86 -0
  24. package/gen-backend/dist/generator/llm-queries/invoke-new-site-analysis.js +169 -0
  25. package/gen-backend/dist/generator/llm-queries/invoke-update-site-schema.js +94 -0
  26. package/gen-backend/dist/generator/site-generator/common-template-creator.js +108 -0
  27. package/gen-backend/dist/generator/site-generator/document-template-creator.js +329 -0
  28. package/gen-backend/dist/generator/site-generator/id-key-provider.js +14 -0
  29. package/gen-backend/dist/generator/site-generator/jt-site-schema-helpers.js +55 -0
  30. package/gen-backend/dist/generator/site-generator/site-generator.js +75 -0
  31. package/gen-backend/dist/generator/site-generator/template-creator-types.js +1 -0
  32. package/gen-backend/dist/generator/site-generator/template-helpers.js +26 -0
  33. package/gen-backend/dist/generator/site-generator/theme-creator.js +180 -0
  34. package/gen-backend/dist/generator/site-generator/theme-css.js +323 -0
  35. package/gen-backend/dist/generator/site-schema-generator/analysis-types.js +1 -0
  36. package/gen-backend/dist/generator/site-schema-generator/create-l10n.js +42 -0
  37. package/gen-backend/dist/generator/site-schema-generator/create-site-schema.js +240 -0
  38. package/gen-backend/dist/generator/site-schema-generator/default-pages.js +38 -0
  39. package/gen-backend/dist/plugin.js +86 -0
  40. package/gen-backend/prompts/0-context.md +9 -0
  41. package/gen-backend/prompts/generate-fake-content-multiple.md +22 -0
  42. package/gen-backend/prompts/generate-fake-content-single.md +16 -0
  43. package/gen-backend/prompts/message-guard.md +89 -0
  44. package/gen-backend/prompts/new-site-1-analysis.md +214 -0
  45. package/gen-backend/prompts/new-site-2-fields.md +50 -0
  46. package/gen-backend/prompts/predefined-fields.json +110 -0
  47. package/gen-backend/prompts/test-message1.txt +1 -0
  48. package/gen-backend/prompts/update-site-schema-1-write-details.md +57 -0
  49. package/gen-backend/prompts/update-site-schema-2-execute.md +77 -0
  50. package/gen-front/dist/gen-front.css +1 -0
  51. package/gen-front/dist/gen-front.eot +0 -0
  52. package/gen-front/dist/gen-front.mjs +998 -0
  53. package/gen-front/dist/gen-front.svg +345 -0
  54. package/gen-front/dist/gen-front.ttf +0 -0
  55. package/gen-front/dist/gen-front.woff +0 -0
  56. package/gen-front/dist/gen-front.woff2 +0 -0
  57. package/gen-front/dist/gen-front2.woff2 +0 -0
  58. package/gen-front/dist/gen-front3.woff2 +0 -0
  59. package/package.json +79 -0
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # @paroicms/site-generator-plugin
2
+
3
+ ParoiCMS Site Generator Plugin.
4
+
5
+ This package is part of [ParoiCMS](https://www.npmjs.com/package/@paroicms/server).
6
+
7
+ ## License
8
+
9
+ Released under the [MIT license](https://gitlab.com/paroi/opensource/paroicms/-/blob/main/LICENSE.md).
@@ -0,0 +1,2 @@
1
+ import { resolveModuleDirectory } from "@paroicms/public-server-lib";
2
+ export const projectDir = resolveModuleDirectory(import.meta.url, { parent: true });
@@ -0,0 +1,37 @@
1
+ import { boolVal, isObj, strVal, strValOrUndef } from "@paroi/data-formatters-lib";
2
+ export function formatGeneratorPluginConfiguration(pluginConf) {
3
+ const anthropicApiKey = strValOrUndef(pluginConf.anthropicApiKey, { varName: "anthropicApiKey" });
4
+ const mistralApiKey = strValOrUndef(pluginConf.mistralApiKey, { varName: "mistralApiKey" });
5
+ const packName = strValOrUndef(pluginConf.packName, { varName: "packName" });
6
+ const debugDir = strValOrUndef(pluginConf.debugDir, { varName: "debugDir" });
7
+ if (!anthropicApiKey || !mistralApiKey || !packName) {
8
+ throw new Error(`Invalid configuration for plugin "@paroicms/site-generator-plugin"`);
9
+ }
10
+ return { anthropicApiKey, mistralApiKey, packName, debugDir };
11
+ }
12
+ export function formatGeneratorCommand(data) {
13
+ if (!isObj(data))
14
+ throw new Error("Invalid input");
15
+ const command = strVal(data.command, { varName: "command" });
16
+ if (command === "createSiteSchema") {
17
+ return {
18
+ command: "createSiteSchema",
19
+ prompt: strVal(data.prompt, { varName: "prompt" }),
20
+ };
21
+ }
22
+ if (command === "updateSiteSchema") {
23
+ return {
24
+ command: "updateSiteSchema",
25
+ prompt: strVal(data.prompt, { varName: "prompt" }),
26
+ generatedSchema: data.generatedSchema,
27
+ };
28
+ }
29
+ if (command === "generateSite") {
30
+ return {
31
+ command: "generateSite",
32
+ generatedSchema: data.generatedSchema,
33
+ withFakeContent: boolVal(data.withFakeContent, { varName: "withFakeContent" }),
34
+ };
35
+ }
36
+ throw new Error(`Invalid command: ${command}`);
37
+ }
@@ -0,0 +1,35 @@
1
+ import { invokeMessageGuard } from "./llm-queries/invoke-message-guard.js";
2
+ import { invokeNewSiteAnalysis } from "./llm-queries/invoke-new-site-analysis.js";
3
+ import { invokeUpdateSiteSchema } from "./llm-queries/invoke-update-site-schema.js";
4
+ import { generateSite } from "./site-generator/site-generator.js";
5
+ export async function executeCommand(ctx, input) {
6
+ if (input.command === "createSiteSchema") {
7
+ const errorResponse = await invokeMessageGuard(ctx, input);
8
+ if (errorResponse)
9
+ return errorResponse;
10
+ return {
11
+ success: true,
12
+ result: await invokeNewSiteAnalysis(ctx, input),
13
+ };
14
+ }
15
+ if (input.command === "updateSiteSchema") {
16
+ const errorResponse = await invokeMessageGuard(ctx, input);
17
+ if (errorResponse)
18
+ return errorResponse;
19
+ return {
20
+ success: true,
21
+ result: await invokeUpdateSiteSchema(ctx, input),
22
+ };
23
+ }
24
+ if (input.command === "generateSite") {
25
+ return {
26
+ success: true,
27
+ result: await generateSite(ctx, input),
28
+ };
29
+ }
30
+ ctx.logger.error(`Invalid command: ${input.command}`);
31
+ return {
32
+ success: false,
33
+ userMessage: "Invalid command",
34
+ };
35
+ }
@@ -0,0 +1,227 @@
1
+ import { getPartTypeByName, getRegularDocumentTypeByName, getRoutingDocumentTypeByName, } from "@paroicms/internal-anywhere-lib";
2
+ import { createSimpleTranslator, } from "@paroicms/public-server-lib";
3
+ import { getRandomImagePath } from "../lib/images-lib.js";
4
+ import { createTaskCollector } from "../lib/tasks.js";
5
+ import { generateLocalizedFooterMention } from "./create-node-contents.js";
6
+ import { generateFieldSetContent, generateMultipleFieldSetContents, } from "./generate-fake-content.js";
7
+ export async function fillSiteWithFakeContent(ctx, { siteConf, siteId, siteTitle, }) {
8
+ const { service } = ctx;
9
+ const { fqdn } = siteConf;
10
+ const { siteSchema, siteIds } = await service.connector.loadSiteSchemaAndIds(fqdn);
11
+ const schemaI18n = createSimpleTranslator({
12
+ labels: siteSchema.l10n,
13
+ logger: ctx.logger,
14
+ });
15
+ await updateSiteFields(ctx, { fqdn, siteSchema, siteId, siteTitle });
16
+ const tasks = createTaskCollector(ctx);
17
+ fillRoutingDocumentAndAddChildren(ctx, tasks, {
18
+ fqdn,
19
+ siteSchema,
20
+ schemaI18n,
21
+ }, {
22
+ routingIds: siteIds.homeIds,
23
+ nodeType: siteSchema.nodeTypes.home,
24
+ });
25
+ const { promise } = tasks.runAll({ maxParallel: 10, rateLimitPerSecond: 3 });
26
+ const { doneCount, errorMessages } = await promise;
27
+ if (errorMessages.length > 0) {
28
+ ctx.logger.warn(`Failed to generate ${errorMessages.length} documents:\n - ${errorMessages.join("\n - ")}`);
29
+ }
30
+ ctx.logger.debug(`… Executed ${doneCount} generating tasks`);
31
+ }
32
+ function fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, nodeOptions) {
33
+ const { routingIds, nodeType } = nodeOptions;
34
+ const { siteSchema } = siteOptions;
35
+ tasks.add(() => updateRoutingDocument(ctx, siteOptions, nodeOptions));
36
+ for (const typeName of nodeType.routingChildren ?? []) {
37
+ const childType = getRoutingDocumentTypeByName(siteSchema, typeName);
38
+ const childIds = routingIds.children?.[typeName];
39
+ if (!childIds)
40
+ throw new Error(`Missing childIds for ${typeName}`);
41
+ fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, {
42
+ routingIds: childIds,
43
+ nodeType: childType,
44
+ });
45
+ }
46
+ for (const typeName of nodeType.regularChildren ?? []) {
47
+ const childType = getRegularDocumentTypeByName(siteSchema, typeName);
48
+ tasks.add(() => addRegularDocuments(ctx, siteOptions, {
49
+ parentNodeId: routingIds.nodeId,
50
+ nodeType: childType,
51
+ }));
52
+ }
53
+ for (const listType of nodeType.lists ?? []) {
54
+ for (const typeName of listType.parts) {
55
+ const childType = getPartTypeByName(siteSchema, typeName);
56
+ tasks.add(() => addParts(ctx, siteOptions, {
57
+ parentNodeId: routingIds.nodeId,
58
+ nodeType: childType,
59
+ }));
60
+ }
61
+ }
62
+ }
63
+ async function updateSiteFields(ctx, options) {
64
+ const { service, logger } = ctx;
65
+ logger.debug("Updating site fields…");
66
+ const { fqdn, siteSchema, siteId, siteTitle } = options;
67
+ const siteType = siteSchema.nodeTypes._site;
68
+ const logoImageFile = getRandomImagePath();
69
+ const fields = [
70
+ [
71
+ "title",
72
+ {
73
+ dataType: "string",
74
+ localized: true,
75
+ value: siteTitle,
76
+ },
77
+ ],
78
+ [
79
+ "contactEmail",
80
+ {
81
+ dataType: "string",
82
+ localized: false,
83
+ value: `${siteId}@yopmail.com`,
84
+ },
85
+ ],
86
+ [
87
+ "favicon",
88
+ {
89
+ dataType: "media",
90
+ localized: false,
91
+ value: {
92
+ file: logoImageFile,
93
+ },
94
+ },
95
+ ],
96
+ [
97
+ "ogImage",
98
+ {
99
+ dataType: "media",
100
+ localized: false,
101
+ value: {
102
+ file: logoImageFile,
103
+ },
104
+ },
105
+ ],
106
+ ];
107
+ if (siteType.fields.some((f) => f.name === "logo")) {
108
+ fields.push([
109
+ "logo",
110
+ {
111
+ dataType: "media",
112
+ localized: false,
113
+ value: {
114
+ file: logoImageFile,
115
+ },
116
+ },
117
+ ]);
118
+ }
119
+ if (siteType.fields.some((f) => f.name === "footerMention")) {
120
+ fields.push([
121
+ "footerMention",
122
+ {
123
+ dataType: "quillDelta",
124
+ localized: true,
125
+ value: generateLocalizedFooterMention(siteSchema),
126
+ },
127
+ ]);
128
+ }
129
+ await service.connector.updateSiteFields(fqdn, Object.fromEntries(fields));
130
+ logger.debug("… Site fields updated");
131
+ }
132
+ async function updateRoutingDocument(ctx, siteOptions, nodeOptions) {
133
+ ctx.logger.debug(`[TASK] Updating routing document "${nodeOptions.nodeType.typeName}"…`);
134
+ const { routingIds, nodeType } = nodeOptions;
135
+ const { fqdn, siteSchema, schemaI18n } = siteOptions;
136
+ const content = await generateFieldSetContent(ctx, {
137
+ nodeType,
138
+ siteSchema,
139
+ schemaI18n,
140
+ withTitle: false,
141
+ debugName: nodeType.kebabName,
142
+ });
143
+ await ctx.service.connector.updateDocumentContent(fqdn, {
144
+ nodeId: routingIds.nodeId,
145
+ content: toNcDocumentContent(content, nodeType),
146
+ });
147
+ }
148
+ async function addRegularDocuments(ctx, siteOptions, nodeOptions) {
149
+ ctx.logger.debug(`[TASK] Adding regular documents "${nodeOptions.nodeType.typeName}"…`);
150
+ const { parentNodeId, nodeType } = nodeOptions;
151
+ const { fqdn, siteSchema, schemaI18n } = siteOptions;
152
+ const tolerateErrors = {
153
+ errorMessages: [],
154
+ };
155
+ const list = await generateMultipleFieldSetContents(ctx, {
156
+ siteSchema,
157
+ nodeType,
158
+ schemaI18n,
159
+ count: getDefaultNodeContentCount(nodeType),
160
+ withTitle: true,
161
+ tolerateErrors,
162
+ debugName: nodeType.kebabName,
163
+ });
164
+ if (tolerateErrors.errorMessages.length > 0) {
165
+ ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${tolerateErrors.errorMessages.join("\n - ")}`);
166
+ }
167
+ await ctx.service.connector.addMultipleDocumentContents(fqdn, {
168
+ parentNodeId,
169
+ contents: list.map((content) => toNcDocumentContent(content, nodeType)),
170
+ });
171
+ }
172
+ async function addParts(ctx, siteOptions, nodeOptions) {
173
+ ctx.logger.debug(`[TASK] Adding parts "${nodeOptions.nodeType.typeName}"…`);
174
+ const { parentNodeId, nodeType } = nodeOptions;
175
+ const { fqdn, siteSchema, schemaI18n } = siteOptions;
176
+ const tolerateErrors = {
177
+ errorMessages: [],
178
+ };
179
+ const list = await generateMultipleFieldSetContents(ctx, {
180
+ siteSchema,
181
+ nodeType,
182
+ schemaI18n,
183
+ count: getDefaultNodeContentCount(nodeType),
184
+ withTitle: true,
185
+ tolerateErrors,
186
+ debugName: nodeType.kebabName,
187
+ });
188
+ if (tolerateErrors.errorMessages.length > 0) {
189
+ ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${tolerateErrors.errorMessages.join("\n - ")}`);
190
+ }
191
+ await ctx.service.connector.addMultiplePartContents(fqdn, {
192
+ parentNodeId,
193
+ contents: list.map((content) => toNcPartContent(content, nodeType)),
194
+ });
195
+ }
196
+ function toNcDocumentContent(content, nodeType) {
197
+ const { title, fields, featuredImage } = content;
198
+ return {
199
+ kind: "document",
200
+ typeName: nodeType.typeName,
201
+ title,
202
+ featuredImage,
203
+ fields,
204
+ };
205
+ }
206
+ function toNcPartContent(content, nodeType) {
207
+ const { fields } = content;
208
+ return {
209
+ kind: "part",
210
+ typeName: nodeType.typeName,
211
+ fields,
212
+ };
213
+ }
214
+ function getDefaultNodeContentCount(nodeType) {
215
+ if (nodeType.kind === "site")
216
+ throw new Error("Cannot generate content for site node type");
217
+ if (nodeType.kind === "document") {
218
+ if (nodeType.documentKind === "routing")
219
+ return 1;
220
+ if (nodeType.route === ":yyyy/:mm/:dd/:relativeId-:slug")
221
+ return 14; // 4
222
+ return 8; // 2
223
+ }
224
+ if (nodeType.kind === "part")
225
+ return 8; // 2
226
+ throw new Error(`Unknown node type kind: ${nodeType.kind}`);
227
+ }
@@ -0,0 +1,156 @@
1
+ import markdownToDelta from "markdown-to-quill-delta";
2
+ import { getRandomImagePath } from "../lib/images-lib.js";
3
+ export function createNodeContents(options) {
4
+ const { nodeType, count, generatedContents, outputTags, language } = options;
5
+ const result = [];
6
+ for (let i = 0; i < count; ++i) {
7
+ const generatedContent = generatedContents ? generatedContents[i] : {};
8
+ if (generatedContent === undefined)
9
+ break; // The LLM produced less than expected
10
+ const nodeContent = createNodeContent({
11
+ nodeType,
12
+ language,
13
+ generatedContent,
14
+ outputTags,
15
+ });
16
+ result.push(nodeContent);
17
+ }
18
+ return result;
19
+ }
20
+ function createNodeContent(options) {
21
+ const { nodeType, language, generatedContent, outputTags } = options;
22
+ const fieldTypes = nodeType.fields ?? [];
23
+ if (nodeType.kind === "document") {
24
+ const { title, ...generatedFields } = generatedContent;
25
+ const fields = toNcFieldSetContent(generatedFields, outputTags, fieldTypes, language);
26
+ const featuredImage = nodeType.withFeaturedImage
27
+ ? {
28
+ file: getRandomImagePath(),
29
+ }
30
+ : undefined;
31
+ return {
32
+ title,
33
+ featuredImage,
34
+ fields,
35
+ };
36
+ }
37
+ const fields = toNcFieldSetContent(generatedContent, outputTags, fieldTypes, language);
38
+ return { fields };
39
+ }
40
+ function toNcFieldSetContent(content, outputTags, fieldTypes, language) {
41
+ const result = {};
42
+ for (const fieldType of fieldTypes) {
43
+ const isMarkdown = outputTags.find((tag) => tag.key === fieldType.name)?.format === "markdown";
44
+ const localized = fieldType.localized;
45
+ const value = content[fieldType.name];
46
+ const fieldContent = toNcFieldContent({
47
+ fieldType,
48
+ generatedValue: value,
49
+ localized,
50
+ language,
51
+ isMarkdown,
52
+ });
53
+ if (fieldContent !== undefined) {
54
+ result[fieldType.name] = fieldContent;
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+ function toNcFieldContent(options) {
60
+ const { fieldType, generatedValue, localized, language, isMarkdown } = options;
61
+ if (generatedValue !== undefined && localized) {
62
+ if (fieldType.dataType === "string") {
63
+ return {
64
+ dataType: "string",
65
+ localized: true,
66
+ value: generatedValue,
67
+ };
68
+ }
69
+ if (fieldType.dataType === "quillDelta" && isMarkdown) {
70
+ return toNcQuillDeltaContent(generatedValue);
71
+ }
72
+ }
73
+ if (fieldType.storedAs === "mediaHandle") {
74
+ if (fieldType.dataType === "media") {
75
+ const file = getRandomImagePath();
76
+ return localized
77
+ ? {
78
+ dataType: "media",
79
+ localized: true,
80
+ value: { [language]: { file } },
81
+ }
82
+ : {
83
+ dataType: "media",
84
+ localized: false,
85
+ value: { file },
86
+ };
87
+ }
88
+ if (fieldType.dataType === "gallery") {
89
+ const files = [
90
+ getRandomImagePath(),
91
+ getRandomImagePath(),
92
+ getRandomImagePath(),
93
+ getRandomImagePath(),
94
+ getRandomImagePath(),
95
+ getRandomImagePath(),
96
+ ];
97
+ return localized
98
+ ? {
99
+ dataType: "gallery",
100
+ localized: true,
101
+ value: { [language]: { files } },
102
+ }
103
+ : {
104
+ dataType: "gallery",
105
+ localized: false,
106
+ value: { files },
107
+ };
108
+ }
109
+ }
110
+ if (fieldType.dataType === "string") {
111
+ const value = getDefaultStringValueForField(fieldType.name);
112
+ if (value !== undefined) {
113
+ return localized
114
+ ? {
115
+ dataType: "string",
116
+ localized: true,
117
+ value: { [language]: value },
118
+ }
119
+ : {
120
+ dataType: "string",
121
+ localized: false,
122
+ value,
123
+ };
124
+ }
125
+ }
126
+ }
127
+ function toNcQuillDeltaContent(content) {
128
+ return {
129
+ dataType: "quillDelta",
130
+ localized: true,
131
+ value: Object.fromEntries(Object.entries(content).map(([language, text]) => [
132
+ language,
133
+ {
134
+ ops: markdownToDelta(text),
135
+ },
136
+ ])),
137
+ };
138
+ }
139
+ function getDefaultStringValueForField(fieldName) {
140
+ if (fieldName === "phone" || fieldName === "phone2")
141
+ return "0123456789";
142
+ if (fieldName === "phones")
143
+ return JSON.stringify(["0123456789", "9876543210"]);
144
+ if (fieldName === "updateDateTime")
145
+ return new Date().toISOString();
146
+ return undefined;
147
+ }
148
+ export function generateLocalizedFooterMention(siteSchema) {
149
+ const { languages, languageLabels } = siteSchema;
150
+ return Object.fromEntries(languages.map((language) => [
151
+ language,
152
+ {
153
+ ops: markdownToDelta(`Powered by ParoiCMS in ${languageLabels[language]}`),
154
+ },
155
+ ]));
156
+ }
@@ -0,0 +1,127 @@
1
+ import { camelToKebabCase, camelToTitleCase } from "../lib/utils.js";
2
+ import { createNodeContents } from "./create-node-contents.js";
3
+ import { invokeGenerateFakeContent, } from "./invoke-generate-fake-content.js";
4
+ export async function generateFieldSetContent(ctx, options) {
5
+ const list = await generateMultipleFieldSetContents(ctx, {
6
+ ...options,
7
+ count: 1,
8
+ });
9
+ if (list.length !== 1)
10
+ throw new Error(`Expected one item, got ${list.length}`);
11
+ return list[0];
12
+ }
13
+ export async function generateMultipleFieldSetContents(ctx, options) {
14
+ const { siteSchema, nodeType, schemaI18n, count, withTitle, tolerateErrors, debugName } = options;
15
+ if (nodeType.kind === "site")
16
+ throw new Error("Cannot generate content for site node type");
17
+ const outputTags = withTitle
18
+ ? [{ tagName: "title", key: "title", format: "text", tagDescription: "Write the title here" }]
19
+ : [];
20
+ if (nodeType.fields) {
21
+ outputTags.push(...nodeType.fields.map(toFakeContentOutputTag).filter(Boolean));
22
+ }
23
+ const defaultLanguage = siteSchema.languages?.[0];
24
+ const typeLabel = translateText(schemaI18n, `nodeTypes.${nodeType.typeName}.label`, {
25
+ defaultValue: camelToTitleCase(nodeType.typeName),
26
+ defaultLanguage,
27
+ });
28
+ const typeDescription = translateText(schemaI18n, `nodeTypes.${nodeType.typeName}.description`, {
29
+ defaultValue: "",
30
+ defaultLanguage,
31
+ });
32
+ const siteTheme = translateText(schemaI18n, "siteTheme", {
33
+ defaultValue: "",
34
+ defaultLanguage,
35
+ });
36
+ const language = defaultLanguage ?? "en";
37
+ const generatedContents = outputTags.length > 0
38
+ ? await invokeGenerateFakeContent(ctx, {
39
+ count,
40
+ typeLabel,
41
+ typeDescription,
42
+ siteTheme,
43
+ language,
44
+ }, outputTags, { tolerateErrors, debugName })
45
+ : undefined;
46
+ return createNodeContents({
47
+ nodeType,
48
+ count,
49
+ generatedContents,
50
+ outputTags,
51
+ language,
52
+ });
53
+ }
54
+ function translateText(schemaI18n, key, options) {
55
+ const defaultValue = options.defaultValue ?? key;
56
+ const val = schemaI18n.translate({
57
+ key,
58
+ language: "en",
59
+ defaultValue,
60
+ });
61
+ if (val !== defaultValue || !options.defaultLanguage)
62
+ return val;
63
+ return schemaI18n.translate({
64
+ key,
65
+ language: options.defaultLanguage,
66
+ defaultValue,
67
+ });
68
+ }
69
+ function toFakeContentOutputTag(fieldNameOrType) {
70
+ const key = typeof fieldNameOrType === "string" ? fieldNameOrType : fieldNameOrType.name;
71
+ if (key === "leadParagraph") {
72
+ return {
73
+ tagName: `${camelToKebabCase(key)}-md`,
74
+ key,
75
+ format: "markdown",
76
+ tagDescription: "Write one lead paragraph here (15-30 words) in markdown format",
77
+ };
78
+ }
79
+ if (key === "htmlContent") {
80
+ return {
81
+ tagName: `${camelToKebabCase(key)}-md`,
82
+ key,
83
+ format: "markdown",
84
+ tagDescription: "Write document content here (150-200 words) in markdown format",
85
+ };
86
+ }
87
+ if (key === "introduction") {
88
+ return {
89
+ tagName: `${camelToKebabCase(key)}-md`,
90
+ key,
91
+ format: "markdown",
92
+ tagDescription: "Write a short introduction here (15-30 words) in markdown format",
93
+ };
94
+ }
95
+ if (key === "footerMention") {
96
+ return {
97
+ tagName: `${camelToKebabCase(key)}-md`,
98
+ key,
99
+ format: "markdown",
100
+ tagDescription: "Write a very short footer mention here in markdown format",
101
+ };
102
+ }
103
+ if (key === "slogan") {
104
+ return {
105
+ tagName: camelToKebabCase(key),
106
+ key,
107
+ format: "text",
108
+ tagDescription: "Write a slogan here in plain text",
109
+ };
110
+ }
111
+ if (key === "title") {
112
+ return {
113
+ tagName: camelToKebabCase(key),
114
+ key,
115
+ format: "text",
116
+ tagDescription: "Write a title here in plain text",
117
+ };
118
+ }
119
+ if (key === "shortTitle") {
120
+ return {
121
+ tagName: camelToKebabCase(key),
122
+ key,
123
+ format: "text",
124
+ tagDescription: "Rewrite the title as a short button label (1 or 2 words) here",
125
+ };
126
+ }
127
+ }
@@ -0,0 +1,49 @@
1
+ import { PromptTemplate } from "@langchain/core/prompts";
2
+ import { languageLabelIn } from "@paroicms/public-anywhere-lib";
3
+ import { readPromptFile } from "../lib/create-prompt.js";
4
+ import { debugBatchLlmOutputs } from "../lib/debug-utils.js";
5
+ import { parseLlmResponseAsList } from "../lib/parse-llm-response.js";
6
+ const singlePromptTpl = PromptTemplate.fromTemplate(await readPromptFile("generate-fake-content-single.md"));
7
+ const multiplePromptTpl = PromptTemplate.fromTemplate(await readPromptFile("generate-fake-content-multiple.md"));
8
+ export async function invokeGenerateFakeContent(ctx, input, outputTags, options) {
9
+ const { language } = input;
10
+ const single = input.count === 1;
11
+ const debugName = `fake-content-${options.debugName}${single ? "" : `-${input.count}`}`;
12
+ const tagAndDescriptions = outputTags
13
+ .map(({ tagName, tagDescription }) => `<${tagName}>${tagDescription}</${tagName}>`)
14
+ .join("\n\n");
15
+ // Call LLM in batch mode
16
+ const maxBatchSize = 15;
17
+ const batchInputs = [];
18
+ let startIndex = 0;
19
+ while (startIndex < input.count) {
20
+ const nextIndex = startIndex + maxBatchSize;
21
+ const end = Math.min(nextIndex, input.count);
22
+ const llmInput = {
23
+ tagAndDescriptions,
24
+ typeLabel: input.typeLabel,
25
+ typeDescription: input.typeDescription,
26
+ siteTheme: input.siteTheme,
27
+ language: languageLabelIn(language, "en"),
28
+ };
29
+ if (!single) {
30
+ llmInput.count = `${end - startIndex}`;
31
+ }
32
+ batchInputs.push(llmInput);
33
+ startIndex = nextIndex;
34
+ }
35
+ const debug = await debugBatchLlmOutputs(ctx, debugName, ctx.cheapModelName, batchInputs);
36
+ let contents = debug.storedContents;
37
+ if (!contents) {
38
+ const llmMessages = await (single ? singlePromptTpl : multiplePromptTpl)
39
+ .pipe(ctx.cheapModel)
40
+ .batch(batchInputs);
41
+ contents = await debug.getMessageContents(llmMessages);
42
+ }
43
+ const results = [];
44
+ for (const llmMessageContent of contents) {
45
+ const list = parseLlmResponseAsList(llmMessageContent, outputTags, options);
46
+ results.push(...list.map((fields) => Object.fromEntries(Object.entries(fields).map(([fieldName, value]) => [fieldName, { [language]: value }]))));
47
+ }
48
+ return results;
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ export function resolveModuleDirectory(moduleUrl, { parent = false } = {}) {
4
+ const dir = resolve(dirname(fileURLToPath(moduleUrl)));
5
+ return parent ? dirname(dir) : dir;
6
+ }
@@ -0,0 +1,14 @@
1
+ export function reorderObjectKeys(obj, keys) {
2
+ const newObj = {};
3
+ for (const key of keys) {
4
+ if (!(key in obj))
5
+ continue;
6
+ newObj[key] = obj[key];
7
+ }
8
+ for (const key of Object.keys(obj)) {
9
+ if (keys.includes(key))
10
+ continue;
11
+ newObj[key] = obj[key];
12
+ }
13
+ return newObj;
14
+ }
@@ -0,0 +1 @@
1
+ export {};