@paroicms/site-generator-plugin 0.7.0 → 0.8.1

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 (28) hide show
  1. package/gen-backend/ddl/site-generator.ddl.sql +22 -0
  2. package/gen-backend/dist/commands/execute-command.js +60 -0
  3. package/gen-backend/dist/{generator/session → commands}/generator-session.js +16 -8
  4. package/gen-backend/dist/context.js +6 -0
  5. package/gen-backend/dist/db/db-init.js +35 -0
  6. package/gen-backend/dist/db/db.queries.js +60 -0
  7. package/gen-backend/dist/db/ddl-migration.js +15 -0
  8. package/gen-backend/dist/generator/fake-content-generator.ts/content-helpers.js +17 -0
  9. package/gen-backend/dist/generator/fake-content-generator.ts/content-report.js +11 -0
  10. package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js +27 -18
  11. package/gen-backend/dist/generator/lib/parse-llm-response.js +10 -16
  12. package/gen-backend/dist/generator/llm-queries/invoke-message-guard.js +2 -0
  13. package/gen-backend/dist/generator/llm-queries/invoke-new-site-analysis.js +28 -6
  14. package/gen-backend/dist/generator/llm-queries/invoke-update-site-schema.js +5 -1
  15. package/gen-backend/dist/generator/site-generator/site-generator.js +14 -11
  16. package/gen-backend/dist/generator/site-schema-generator/create-site-schema.js +1 -0
  17. package/gen-backend/dist/lib/site-remover.js +39 -0
  18. package/gen-backend/dist/plugin.js +43 -25
  19. package/gen-backend/prompts/0-context.md +2 -2
  20. package/gen-backend/prompts/new-site-1-analysis.md +20 -5
  21. package/gen-backend/prompts/new-site-2-fields.md +1 -0
  22. package/gen-backend/prompts/update-site-schema-2-execute.md +20 -4
  23. package/gen-front/dist/gen-front.css +1 -1
  24. package/gen-front/dist/gen-front.woff2 +0 -0
  25. package/gen-front/dist/gen-front2.woff2 +0 -0
  26. package/package.json +7 -5
  27. package/gen-backend/dist/generator/actions.js +0 -45
  28. /package/gen-backend/dist/{generator/generator-types.js → lib/internal-types.js} +0 -0
@@ -0,0 +1,22 @@
1
+ PRAGMA journal_mode = TRUNCATE;
2
+ PRAGMA foreign_keys = 1;
3
+
4
+ create table PaMetadata (
5
+ dbSchema varchar(100) not null,
6
+ k varchar(100) not null,
7
+ val varchar(250) not null,
8
+ primary key (dbSchema, k)
9
+ );
10
+
11
+ insert into PaMetadata (dbSchema, k, val) values ('site-generator', 'dbSchemaVersion', '1');
12
+
13
+ create table PaGenSession (
14
+ id char(36) not null primary key,
15
+ createdAt timestamp not null default current_timestamp,
16
+ status varchar(100) not null,
17
+ guardCount integer not null default 0,
18
+ promptCount integer not null default 0,
19
+ nodeTypeCount integer not null default 0,
20
+ contentCount integer not null default 0,
21
+ errorMessage varchar(500)
22
+ );
@@ -0,0 +1,60 @@
1
+ import { messageOf } from "@paroi/data-formatters-lib";
2
+ import { updateSession } from "../db/db.queries.js";
3
+ import { invokeMessageGuard } from "../generator/llm-queries/invoke-message-guard.js";
4
+ import { invokeNewSiteAnalysis } from "../generator/llm-queries/invoke-new-site-analysis.js";
5
+ import { invokeUpdateSiteSchema } from "../generator/llm-queries/invoke-update-site-schema.js";
6
+ import { generateSite } from "../generator/site-generator/site-generator.js";
7
+ import { newSession, verifySessionToken } from "./generator-session.js";
8
+ export async function executeCommand(commandCtx, input) {
9
+ if (input.command === "newSession") {
10
+ return {
11
+ success: true,
12
+ result: await newSession(commandCtx, input),
13
+ };
14
+ }
15
+ const { sessionId } = await verifySessionToken(commandCtx, input.sessionToken);
16
+ const ctx = {
17
+ ...commandCtx,
18
+ sessionId,
19
+ };
20
+ try {
21
+ if (input.command === "createSiteSchema") {
22
+ const errorResponse = await invokeMessageGuard(ctx, input);
23
+ if (errorResponse)
24
+ return errorResponse;
25
+ return {
26
+ success: true,
27
+ result: await invokeNewSiteAnalysis(ctx, input),
28
+ };
29
+ }
30
+ if (input.command === "updateSiteSchema") {
31
+ const errorResponse = await invokeMessageGuard(ctx, input, { skipOutOfScopeCheck: true });
32
+ if (errorResponse)
33
+ return errorResponse;
34
+ return {
35
+ success: true,
36
+ result: await invokeUpdateSiteSchema(ctx, input),
37
+ };
38
+ }
39
+ if (input.command === "generateSite") {
40
+ return {
41
+ success: true,
42
+ result: await generateSite(ctx, input),
43
+ };
44
+ }
45
+ ctx.logger.error(`Invalid command: ${input.command}`);
46
+ return {
47
+ success: false,
48
+ userMessage: "Invalid command",
49
+ };
50
+ }
51
+ catch (error) {
52
+ try {
53
+ await updateSession(ctx, { status: "error", errorMessage: messageOf(error) });
54
+ }
55
+ catch {
56
+ ctx.logger.error("Failed to update session error message", error);
57
+ }
58
+ throw error;
59
+ }
60
+ }
@@ -1,24 +1,25 @@
1
1
  import { ApiError } from "@paroicms/public-server-lib";
2
- import { nanoid } from "nanoid";
2
+ import { randomUUID } from "node:crypto";
3
+ import { insertSession, readSession } from "../db/db.queries.js";
3
4
  const { sign, verify } = (await import("jsonwebtoken")).default;
4
- const JWT_SECRET = "init"; // FIXME: Hardcoded JWT secret as specified
5
5
  export async function newSession(ctx, command) {
6
6
  const { service } = ctx;
7
7
  const { recaptchaToken } = command;
8
8
  const isValid = await service.validateRecaptchaResponse(recaptchaToken);
9
9
  if (!isValid)
10
10
  throw new ApiError("Invalid reCAPTCHA token", 400);
11
- const sessionId = nanoid();
12
- const token = sign({ sessionId }, JWT_SECRET, { expiresIn: "24h" });
11
+ const sessionId = randomUUID();
12
+ const token = sign({ sessionId }, ctx.jwtSecret, { expiresIn: "48h" });
13
+ await insertSession(ctx, { sessionId });
13
14
  return { token };
14
15
  }
15
16
  /**
16
17
  * Verifies a session token and returns the session ID if valid
17
18
  */
18
- export function verifySessionToken(token) {
19
+ export async function verifySessionToken(ctx, token) {
19
20
  let payload;
20
21
  try {
21
- payload = verify(token, JWT_SECRET);
22
+ payload = verify(token, ctx.jwtSecret);
22
23
  }
23
24
  catch (error) {
24
25
  if (error.name === "TokenExpiredError")
@@ -27,7 +28,14 @@ export function verifySessionToken(token) {
27
28
  throw new ApiError("Invalid session token", 401);
28
29
  throw error;
29
30
  }
30
- if (!payload || !payload.sessionId)
31
+ if (!payload || !("sessionId" in payload) || typeof payload.sessionId !== "string") {
31
32
  throw new ApiError("Invalid session token", 401);
32
- return payload.sessionId;
33
+ }
34
+ const sessionId = payload.sessionId;
35
+ const session = await readSession(ctx, sessionId);
36
+ if (!session)
37
+ throw new ApiError(`Missing session "${sessionId}"`, 500);
38
+ if (session.promptCount >= 20)
39
+ throw new ApiError("Session consumed", 401);
40
+ return { sessionId };
33
41
  }
@@ -1,2 +1,8 @@
1
+ import { strVal } from "@paroi/data-formatters-lib";
1
2
  import { resolveModuleDirectory } from "@paroicms/public-server-lib";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
2
5
  export const projectDir = resolveModuleDirectory(import.meta.url, { parent: true });
6
+ export const packageDir = dirname(projectDir);
7
+ export const pluginVersion = strVal(JSON.parse(readFileSync(join(packageDir, "package.json"), "utf-8")).version);
8
+ export const SLUG = "site-generator";
@@ -0,0 +1,35 @@
1
+ import { createOrOpenSqliteConnection, createSqlLogger, getMetadataDbSchemaVersion, } from "@paroicms/internal-server-lib";
2
+ import { join } from "node:path";
3
+ import { projectDir } from "../context.js";
4
+ import { currentDbSchemaVersion, dbSchemaName, migrateSiteGeneratorDb } from "./ddl-migration.js";
5
+ export async function createOrOpenSiteGeneratorConnection({ sqliteFile, canCreate, logger, }) {
6
+ const { logNextQuery, knexLogger } = createSqlLogger({ logger, dbSchemaName });
7
+ const { cn } = await createOrOpenSqliteConnection({
8
+ canCreate,
9
+ dbSchemaName,
10
+ sqliteFile,
11
+ ddlFile: join(projectDir, "ddl", "site-generator.ddl.sql"),
12
+ migrateDb,
13
+ knexLogger,
14
+ logger,
15
+ });
16
+ async function migrateDb(cn) {
17
+ const dbVersion = await getMetadataDbSchemaVersion(cn, { dbSchemaName });
18
+ if (dbVersion === currentDbSchemaVersion) {
19
+ return {
20
+ migrated: false,
21
+ schemaVersion: dbVersion,
22
+ };
23
+ }
24
+ await migrateSiteGeneratorDb(cn, {
25
+ fromVersion: dbVersion,
26
+ logger,
27
+ });
28
+ return {
29
+ migrated: true,
30
+ fromVersion: dbVersion,
31
+ schemaVersion: currentDbSchemaVersion,
32
+ };
33
+ }
34
+ return { cn, logNextQuery };
35
+ }
@@ -0,0 +1,60 @@
1
+ import { strVal, strValOrUndef } from "@paroi/data-formatters-lib";
2
+ export async function readSession(ctx, sessionId) {
3
+ const { cn } = ctx;
4
+ const row = await cn("PaGenSession")
5
+ .select("id", "createdAt", "status", "guardCount", "promptCount", "nodeTypeCount", "contentCount", "errorMessage")
6
+ .where({ id: sessionId })
7
+ .first();
8
+ if (!row)
9
+ return;
10
+ return {
11
+ id: strVal(row.id),
12
+ createdAt: strVal(row.createdAt),
13
+ status: formatSessionStatus(row.status),
14
+ guardCount: Number(row.guardCount),
15
+ promptCount: Number(row.promptCount),
16
+ nodeTypeCount: Number(row.nodeTypeCount),
17
+ contentCount: Number(row.contentCount),
18
+ errorMessage: strValOrUndef(row.errorMessage),
19
+ };
20
+ }
21
+ export async function insertSession(ctx, { sessionId }) {
22
+ const { cn } = ctx;
23
+ await cn("PaGenSession").insert({
24
+ id: sessionId,
25
+ status: "started",
26
+ });
27
+ }
28
+ export async function updateSession(ctx, values) {
29
+ const { sessionId, cn } = ctx;
30
+ const { status, guardCountInc, promptCountInc, nodeTypeCount, contentCountInc, errorMessage } = values;
31
+ const affected = await cn("PaGenSession")
32
+ .where({
33
+ id: sessionId,
34
+ })
35
+ .update({
36
+ status,
37
+ guardCountInc: guardCountInc ? cn.raw(`guardCount + ${guardCountInc}`) : undefined,
38
+ promptCount: promptCountInc ? cn.raw(`promptCount + ${promptCountInc}`) : undefined,
39
+ nodeTypeCount,
40
+ contentCount: contentCountInc ? cn.raw(`contentCount + ${contentCountInc}`) : undefined,
41
+ errorMessage,
42
+ });
43
+ if (affected !== 1) {
44
+ throw new Error(`Session "${sessionId}" not found`);
45
+ }
46
+ }
47
+ function formatSessionStatus(val) {
48
+ const v = strVal(val);
49
+ if (v === "started")
50
+ return "started";
51
+ if (v === "analyzed")
52
+ return "analyzed";
53
+ if (v === "updated")
54
+ return "updated";
55
+ if (v === "generated")
56
+ return "generated";
57
+ if (v === "error")
58
+ return "error";
59
+ throw new Error(`Invalid session status: "${val}"`);
60
+ }
@@ -0,0 +1,15 @@
1
+ export const dbSchemaName = "site-generator";
2
+ export const currentDbSchemaVersion = 1;
3
+ export async function migrateSiteGeneratorDb(_cn, { fromVersion, logger }) {
4
+ const toVersion = currentDbSchemaVersion;
5
+ const currentVersion = fromVersion;
6
+ if (currentVersion === 1) {
7
+ // await cn.raw("...");
8
+ // await setMetadataDbSchemaVersion(cn, { dbSchemaName, value: 2 });
9
+ // currentVersion = 2;
10
+ }
11
+ if (currentVersion !== toVersion) {
12
+ throw new Error(`version of ${dbSchemaName} database should be '${toVersion}', but is '${currentVersion}'`);
13
+ }
14
+ logger.info(`${dbSchemaName} database was migrated from ${fromVersion} to ${currentVersion}`);
15
+ }
@@ -0,0 +1,17 @@
1
+ export function dedupMessages(messages) {
2
+ const counters = new Map();
3
+ const result = [];
4
+ for (const m of messages) {
5
+ const counter = counters.get(m);
6
+ if (counter) {
7
+ counters.set(m, counter + 1);
8
+ continue;
9
+ }
10
+ counters.set(m, 1);
11
+ result.push(m);
12
+ }
13
+ return result.map((m) => {
14
+ const counter = counters.get(m);
15
+ return counter && counter > 1 ? `${m} (×${counter})` : m;
16
+ });
17
+ }
@@ -0,0 +1,11 @@
1
+ export function createGeneratedContentReport() {
2
+ let contentCount = 0;
3
+ return {
4
+ getContentCount() {
5
+ return contentCount;
6
+ },
7
+ addContentCount(count) {
8
+ contentCount += count;
9
+ },
10
+ };
11
+ }
@@ -2,19 +2,22 @@ import { getPartTypeByName, getRegularDocumentTypeByName, getRoutingDocumentType
2
2
  import { createSimpleTranslator, } from "@paroicms/public-server-lib";
3
3
  import { getRandomImagePath } from "../lib/images-lib.js";
4
4
  import { createTaskCollector } from "../lib/tasks.js";
5
+ import { dedupMessages } from "./content-helpers.js";
6
+ import { createGeneratedContentReport } from "./content-report.js";
5
7
  import { generateLocalizedFooterMention } from "./create-node-contents.js";
6
8
  import { generateFieldSetContent, generateMultipleFieldSetContents, } from "./generate-fake-content.js";
7
- export async function fillSiteWithFakeContent(ctx, { siteConf, siteId, siteTitle, }) {
9
+ export async function fillSiteWithFakeContent(ctx, { regSite, siteTitle }) {
8
10
  const { service } = ctx;
9
- const { fqdn } = siteConf;
11
+ const { fqdn } = regSite;
12
+ const report = createGeneratedContentReport();
10
13
  const { siteSchema, siteIds } = await service.connector.loadSiteSchemaAndIds(fqdn);
11
14
  const schemaI18n = createSimpleTranslator({
12
15
  labels: siteSchema.l10n,
13
16
  logger: ctx.logger,
14
17
  });
15
- await updateSiteFields(ctx, { fqdn, siteSchema, siteId, siteTitle });
18
+ await updateSiteFields(ctx, report, { fqdn, siteSchema, siteTitle });
16
19
  const tasks = createTaskCollector(ctx);
17
- fillRoutingDocumentAndAddChildren(ctx, tasks, {
20
+ fillRoutingDocumentAndAddChildren(ctx, tasks, report, {
18
21
  fqdn,
19
22
  siteSchema,
20
23
  schemaI18n,
@@ -28,15 +31,16 @@ export async function fillSiteWithFakeContent(ctx, { siteConf, siteId, siteTitle
28
31
  ctx.logger.warn(`Failed to generate ${errorMessages.length} documents:\n - ${errorMessages.join("\n - ")}`);
29
32
  }
30
33
  ctx.logger.debug(`… Executed ${doneCount} generating tasks`);
34
+ return report;
31
35
  }
32
- function fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, nodeOptions) {
36
+ function fillRoutingDocumentAndAddChildren(ctx, tasks, report, siteOptions, nodeOptions) {
33
37
  const { routingIds, nodeType } = nodeOptions;
34
38
  const { siteSchema } = siteOptions;
35
- tasks.add(() => updateRoutingDocument(ctx, siteOptions, nodeOptions));
39
+ tasks.add(() => updateRoutingDocument(ctx, report, siteOptions, nodeOptions));
36
40
  for (const listType of nodeType.lists ?? []) {
37
41
  for (const typeName of listType.parts) {
38
42
  const partType = getPartTypeByName(siteSchema, typeName);
39
- tasks.add(() => addParts(ctx, siteOptions, {
43
+ tasks.add(() => addParts(ctx, report, siteOptions, {
40
44
  parentNodeId: routingIds.nodeId,
41
45
  nodeType: partType,
42
46
  documentType: nodeType,
@@ -48,24 +52,24 @@ function fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, nodeOptions)
48
52
  const childIds = routingIds.children?.[typeName];
49
53
  if (!childIds)
50
54
  throw new Error(`Missing childIds for ${typeName}`);
51
- fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, {
55
+ fillRoutingDocumentAndAddChildren(ctx, tasks, report, siteOptions, {
52
56
  routingIds: childIds,
53
57
  nodeType: childType,
54
58
  });
55
59
  }
56
60
  for (const typeName of nodeType.regularChildren ?? []) {
57
61
  const childType = getRegularDocumentTypeByName(siteSchema, typeName);
58
- tasks.add(() => addRegularDocuments(ctx, siteOptions, {
62
+ tasks.add(() => addRegularDocuments(ctx, report, siteOptions, {
59
63
  parentNodeId: routingIds.nodeId,
60
64
  nodeType: childType,
61
65
  documentType: childType,
62
66
  }));
63
67
  }
64
68
  }
65
- async function updateSiteFields(ctx, options) {
69
+ async function updateSiteFields(ctx, _report, options) {
66
70
  const { service, logger } = ctx;
67
71
  logger.debug("Updating site fields…");
68
- const { fqdn, siteSchema, siteId, siteTitle } = options;
72
+ const { fqdn, siteSchema, siteTitle } = options;
69
73
  const siteType = siteSchema.nodeTypes._site;
70
74
  const logoImageFile = getRandomImagePath();
71
75
  const fields = [
@@ -123,7 +127,7 @@ async function updateSiteFields(ctx, options) {
123
127
  await service.connector.updateSiteFields(fqdn, Object.fromEntries(fields));
124
128
  logger.debug("… Site fields updated");
125
129
  }
126
- async function updateRoutingDocument(ctx, siteOptions, nodeOptions) {
130
+ async function updateRoutingDocument(ctx, report, siteOptions, nodeOptions) {
127
131
  ctx.logger.debug(`[TASK] Updating routing document "${nodeOptions.nodeType.typeName}"…`);
128
132
  const { routingIds, nodeType } = nodeOptions;
129
133
  const { fqdn, siteSchema, schemaI18n } = siteOptions;
@@ -139,8 +143,9 @@ async function updateRoutingDocument(ctx, siteOptions, nodeOptions) {
139
143
  nodeId: routingIds.nodeId,
140
144
  content: toRiDocumentContent(content, nodeType),
141
145
  });
146
+ report.addContentCount(1);
142
147
  }
143
- async function addRegularDocuments(ctx, siteOptions, nodeOptions) {
148
+ async function addRegularDocuments(ctx, report, siteOptions, nodeOptions) {
144
149
  ctx.logger.debug(`[TASK] Adding regular documents "${nodeOptions.nodeType.typeName}"…`);
145
150
  const { parentNodeId, nodeType, documentType } = nodeOptions;
146
151
  const { fqdn, siteSchema, schemaI18n } = siteOptions;
@@ -157,15 +162,17 @@ async function addRegularDocuments(ctx, siteOptions, nodeOptions) {
157
162
  tolerateErrors,
158
163
  debugName: nodeType.kebabName,
159
164
  });
160
- if (tolerateErrors.errorMessages.length > 0) {
161
- ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${tolerateErrors.errorMessages.join("\n - ")}`);
165
+ const errorMessages = dedupMessages(tolerateErrors.errorMessages);
166
+ if (errorMessages.length > 0) {
167
+ ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${errorMessages.join("\n - ")}`);
162
168
  }
163
169
  await ctx.service.connector.addMultipleDocumentContents(fqdn, {
164
170
  parentNodeId,
165
171
  contents: list.map((content) => toRiDocumentContent(content, nodeType)),
166
172
  });
173
+ report.addContentCount(list.length);
167
174
  }
168
- async function addParts(ctx, siteOptions, nodeOptions) {
175
+ async function addParts(ctx, report, siteOptions, nodeOptions) {
169
176
  ctx.logger.debug(`[TASK] Adding parts "${nodeOptions.nodeType.typeName}"…`);
170
177
  const { parentNodeId, nodeType, documentType } = nodeOptions;
171
178
  const { fqdn, siteSchema, schemaI18n } = siteOptions;
@@ -182,13 +189,15 @@ async function addParts(ctx, siteOptions, nodeOptions) {
182
189
  tolerateErrors,
183
190
  debugName: nodeType.kebabName,
184
191
  });
185
- if (tolerateErrors.errorMessages.length > 0) {
186
- ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${tolerateErrors.errorMessages.join("\n - ")}`);
192
+ const errorMessages = dedupMessages(tolerateErrors.errorMessages);
193
+ if (errorMessages.length > 0) {
194
+ ctx.logger.warn(`Error generating content for ${nodeType.typeName}:\n - ${errorMessages.join("\n - ")}`);
187
195
  }
188
196
  await ctx.service.connector.addMultiplePartContents(fqdn, {
189
197
  parentNodeId,
190
198
  contents: list.map((content) => toRiPartContent(content, nodeType)),
191
199
  });
200
+ report.addContentCount(list.length);
192
201
  }
193
202
  function toRiDocumentContent(content, nodeType) {
194
203
  const { title, fields, featuredImage } = content;
@@ -106,9 +106,9 @@ export function parseLlmRawTags(llmResponse, tagNames, options = {}) {
106
106
  const current = matches[i];
107
107
  if (current.isOpening) {
108
108
  // Find the next corresponding closing tag
109
- let j = i + 1;
109
+ const j = i + 1;
110
110
  let foundClosing = false;
111
- while (j < matches.length) {
111
+ if (j < matches.length) {
112
112
  const next = matches[j];
113
113
  // If we encounter another opening tag of any type before finding our closing tag,
114
114
  // it's an error if not tolerating errors
@@ -119,9 +119,15 @@ export function parseLlmRawTags(llmResponse, tagNames, options = {}) {
119
119
  tolerateErrors.errorMessages.push(message);
120
120
  foundClosing = undefined;
121
121
  // If we are tolerating errors, we skip this opening tag entirely
122
- break;
123
122
  }
124
- if (!next.isOpening && next.tagName === current.tagName) {
123
+ else {
124
+ if (next.tagName !== current.tagName) {
125
+ // Found a non-matching closing tag
126
+ const message = `Mismatched tags: opening <${current.tagName}>, closing </${next.tagName}>`;
127
+ if (!tolerateErrors)
128
+ throw new Error(message);
129
+ tolerateErrors.errorMessages.push(message);
130
+ }
125
131
  // Found a matching closing tag
126
132
  const contentStart = current.position + `<${current.tagName}>`.length;
127
133
  const contentEnd = next.position;
@@ -133,19 +139,7 @@ export function parseLlmRawTags(llmResponse, tagNames, options = {}) {
133
139
  // Skip to after this closing tag
134
140
  i = j;
135
141
  foundClosing = true;
136
- break;
137
142
  }
138
- if (!next.isOpening && next.tagName !== current.tagName) {
139
- // Found a non-matching closing tag
140
- const message = `Mismatched tags: opening <${current.tagName}>, closing </${next.tagName}>`;
141
- if (!tolerateErrors)
142
- throw new Error(message);
143
- tolerateErrors.errorMessages.push(message);
144
- foundClosing = undefined;
145
- // If we are tolerating errors, we skip this current opening tag entirely
146
- break;
147
- }
148
- ++j;
149
143
  }
150
144
  // Handle case where no matching closing tag was found
151
145
  if (foundClosing === false) {
@@ -1,4 +1,5 @@
1
1
  import { boolVal, listValOrUndef, strVal, strValOrUndef, } from "@paroi/data-formatters-lib";
2
+ import { updateSession } from "../../db/db.queries.js";
2
3
  import { createPromptTemplate } from "../lib/create-prompt.js";
3
4
  import { debugLlmOutput } from "../lib/debug-utils.js";
4
5
  import { parseLlmResponseAsProperties } from "../lib/parse-llm-response.js";
@@ -35,6 +36,7 @@ export async function invokeMessageGuard(ctx, input, { skipOutOfScopeCheck = fal
35
36
  }
36
37
  if (report.valid)
37
38
  return;
39
+ await updateSession(ctx, { guardCountInc: 1 });
38
40
  return {
39
41
  success: false,
40
42
  language: report.language,
@@ -1,3 +1,4 @@
1
+ import { updateSession } from "../../db/db.queries.js";
1
2
  import { reorderObjectKeys } from "../helpers/js-utils.js";
2
3
  import { createPromptTemplate, getPredefinedFields, getSiteSchemaTsDefs, } from "../lib/create-prompt.js";
3
4
  import { debugLlmOutput } from "../lib/debug-utils.js";
@@ -17,13 +18,14 @@ const fieldsPrompt = await createPromptTemplate({
17
18
  export async function invokeNewSiteAnalysis(ctx, input) {
18
19
  const { analysis, explanation, unusedInformation } = await invokeAnalysisStep1(ctx, input);
19
20
  const siteSchema = createSiteSchemaFromAnalysis(analysis);
20
- await invokeAnalysisStep2(ctx, { prompt: unusedInformation ?? "" }, siteSchema);
21
+ const { unusedInformation: unusedInformation2 } = await invokeAnalysisStep2(ctx, { prompt: createUnusedInformationPrompt(unusedInformation, analysis) ?? "" }, siteSchema);
21
22
  reorderSiteSchemaNodeTypes(siteSchema);
22
23
  const l10n = createL10n(analysis, siteSchema);
23
24
  const siteTitle = {
24
25
  [analysis.siteProperties.language]: analysis.siteProperties.title,
25
26
  };
26
- if (!unusedInformation) {
27
+ if (!unusedInformation2) {
28
+ await updateSession(ctx, { status: "analyzed", promptCountInc: 1 });
27
29
  return {
28
30
  siteTitle,
29
31
  siteSchema,
@@ -32,15 +34,16 @@ export async function invokeNewSiteAnalysis(ctx, input) {
32
34
  explanation,
33
35
  };
34
36
  }
35
- ctx.logger.debug("Unused information:", unusedInformation);
37
+ ctx.logger.debug("Unused information:", unusedInformation2);
36
38
  const updated = await invokeUpdateSiteSchema(ctx, {
37
- prompt: unusedInformation,
39
+ prompt: unusedInformation2,
38
40
  generatedSchema: {
39
41
  siteTitle,
40
42
  siteSchema,
41
43
  l10n,
42
44
  },
43
- });
45
+ }, { asRemainingPrompt: true });
46
+ await updateSession(ctx, { status: "analyzed", promptCountInc: 1 });
44
47
  return {
45
48
  siteTitle,
46
49
  siteSchema: updated.siteSchema,
@@ -122,16 +125,23 @@ siteSchema) {
122
125
  const llmMessage = await fieldsPrompt.pipe(ctx.goodModel).invoke(llmInput);
123
126
  llmMessageContent = await debug.getMessageContent(llmMessage);
124
127
  }
125
- const { assignedFields } = parseLlmResponseAsProperties(llmMessageContent, [
128
+ const { assignedFields, unusedInformation } = parseLlmResponseAsProperties(llmMessageContent, [
126
129
  {
127
130
  tagName: "yaml_result",
128
131
  key: "assignedFields",
129
132
  format: "yaml",
130
133
  },
134
+ {
135
+ tagName: "unused_information_md",
136
+ key: "unusedInformation",
137
+ format: "markdown",
138
+ optional: true,
139
+ },
131
140
  ]);
132
141
  if (siteSchema.nodeTypes) {
133
142
  assignFieldsToNodeTypes(ctx, assignedFields, siteSchema.nodeTypes);
134
143
  }
144
+ return { unusedInformation };
135
145
  }
136
146
  function assignFieldsToNodeTypes(ctx, assignedFields, nodeTypes) {
137
147
  const remainingTypeNames = new Set(Object.keys(assignedFields));
@@ -169,3 +179,15 @@ function reorderSiteSchemaNodeTypes(siteSchema) {
169
179
  "orderChildrenBy",
170
180
  ]));
171
181
  }
182
+ function createUnusedInformationPrompt(unusedInformation, analysis) {
183
+ const prompts = Object.entries(analysis.dictionary)
184
+ .map(([typeName, entry]) => {
185
+ return entry.prompt ? `${typeName}: ${entry.prompt}` : undefined;
186
+ })
187
+ .filter(Boolean);
188
+ if (prompts.length > 0) {
189
+ const nodeTypePrompts = `To do:\n\n- ${prompts.join("- \n")}`;
190
+ return unusedInformation ? `${nodeTypePrompts}\n\n${unusedInformation}` : nodeTypePrompts;
191
+ }
192
+ return unusedInformation;
193
+ }
@@ -1,3 +1,4 @@
1
+ import { updateSession } from "../../db/db.queries.js";
1
2
  import { createPromptTemplate, getPredefinedFields, getSiteSchemaTsDefs, } from "../lib/create-prompt.js";
2
3
  import { debugLlmOutput } from "../lib/debug-utils.js";
3
4
  import { parseLlmResponseAsProperties } from "../lib/parse-llm-response.js";
@@ -7,7 +8,7 @@ const prompt1Tpl = await createPromptTemplate({
7
8
  const prompt2Tpl = await createPromptTemplate({
8
9
  fileName: "update-site-schema-2-execute.md",
9
10
  });
10
- export async function invokeUpdateSiteSchema(ctx, input) {
11
+ export async function invokeUpdateSiteSchema(ctx, input, { asRemainingPrompt = false } = {}) {
11
12
  const task = await invokeUpdateSiteSchemaStep1(ctx, input);
12
13
  if (!task.taskDetailsMd) {
13
14
  // no changes
@@ -21,6 +22,9 @@ export async function invokeUpdateSiteSchema(ctx, input) {
21
22
  taskDetailsMd: task.taskDetailsMd,
22
23
  generatedSchema: input.generatedSchema,
23
24
  });
25
+ if (!asRemainingPrompt) {
26
+ await updateSession(ctx, { status: "updated", promptCountInc: 1 });
27
+ }
24
28
  return {
25
29
  ...genSchema,
26
30
  changed: true,
@@ -1,17 +1,13 @@
1
1
  import { generateSlug } from "@paroicms/public-anywhere-lib";
2
- import { randomUUID } from "node:crypto";
3
2
  import { mkdir, writeFile } from "node:fs/promises";
4
3
  import { join } from "node:path";
4
+ import { updateSession } from "../../db/db.queries.js";
5
5
  import { fillSiteWithFakeContent } from "../fake-content-generator.ts/create-database-with-fake-content.js";
6
6
  import { createTheme } from "./theme-creator.js";
7
7
  export async function generateSite(ctx, input) {
8
- const { service, logger, packConf } = ctx;
9
- const { sitesDir, packName } = packConf;
8
+ const { service, logger, sitesDir, packConf: { packName }, } = ctx;
10
9
  const { generatedSchema: { l10n, siteSchema, siteTitle }, withFakeContent, } = input;
11
- if (!sitesDir) {
12
- throw new Error(`Site-generator plugin can generate sites only for pack with "sitesDir", but pack "${packName}" doesn't have it`);
13
- }
14
- const siteId = randomUUID();
10
+ const siteId = ctx.sessionId;
15
11
  logger.info(`Generating site: ${siteId}…`);
16
12
  const siteDir = join(sitesDir, siteId);
17
13
  await mkdir(siteDir);
@@ -23,14 +19,17 @@ export async function generateSite(ctx, input) {
23
19
  siteTitle: siteTitle.en ?? Object.values(siteTitle)[0] ?? "new-website",
24
20
  }), null, 2), "utf-8");
25
21
  await createTheme(ctx, siteDir, siteSchema);
26
- const siteConf = await service.registerNewSite({
22
+ const regSite = await service.connector.registerNewSite({
27
23
  packName,
28
24
  siteDir,
29
25
  domain: siteId,
30
26
  version: "0.0.0",
31
27
  });
32
28
  if (withFakeContent) {
33
- await fillSiteWithFakeContent(ctx, { siteConf, siteId, siteTitle });
29
+ const report = await fillSiteWithFakeContent(ctx, { regSite, siteTitle });
30
+ await updateSession(ctx, {
31
+ contentCountInc: report.getContentCount(),
32
+ });
34
33
  }
35
34
  const account = {
36
35
  kind: "local",
@@ -38,8 +37,12 @@ export async function generateSite(ctx, input) {
38
37
  name: "Admin",
39
38
  password: Math.random().toString(36).substring(2, 6), // 4 random lowercase characters,
40
39
  };
41
- await ctx.service.connector.createAccount(siteConf.fqdn, account, { asContactEmail: true });
42
- const { siteUrl } = siteConf;
40
+ await ctx.service.connector.createAccount(regSite.fqdn, account, { asContactEmail: true });
41
+ const { siteUrl } = regSite;
42
+ await updateSession(ctx, {
43
+ status: "generated",
44
+ nodeTypeCount: siteSchema.nodeTypes?.length ?? 0,
45
+ });
43
46
  return {
44
47
  siteId,
45
48
  url: siteUrl,
@@ -9,6 +9,7 @@ export function createSiteSchemaFromAnalysis(analysis) {
9
9
  "@paroicms/content-loading-plugin",
10
10
  "@paroicms/public-menu-plugin",
11
11
  "@paroicms/contact-form-plugin",
12
+ "@paroicms/video-plugin",
12
13
  ],
13
14
  nodeTypes: [
14
15
  {