@saltcorn/copilot 0.8.1 → 0.8.3
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/actions/generate-page.js +6 -2
- package/actions/generate-tables.js +70 -6
- package/actions/generate-workflow.js +54 -3
- package/agent-skills/pagegen.js +171 -59
- package/agent-skills/registry-editor.js +30 -2
- package/agent-skills/triggergen.js +1 -5
- package/agent-skills/viewgen.js +49 -7
- package/app-constructor/common.js +7 -1
- package/app-constructor/errors.js +749 -61
- package/app-constructor/feedback-action.js +62 -60
- package/app-constructor/feedback.js +1294 -67
- package/app-constructor/fixed-prompts.js +829 -0
- package/app-constructor/phases.js +1485 -0
- package/app-constructor/prompt-generator.js +587 -0
- package/app-constructor/requirements.js +171 -50
- package/app-constructor/research.js +350 -0
- package/app-constructor/run_task.js +234 -73
- package/app-constructor/schema.js +173 -169
- package/app-constructor/tasks.js +96 -537
- package/app-constructor/tools.js +17 -4
- package/app-constructor/view.js +314 -54
- package/builder-gen.js +90 -41
- package/builder-schema.js +6 -0
- package/copilot-as-agent.js +1 -0
- package/index.js +0 -1
- package/js-code-gen.js +1 -0
- package/package.json +1 -1
- package/relation-paths.js +73 -40
- package/standard-prompt.js +1 -0
- package/user-copilot.js +2 -0
- package/workflow-gen.js +1 -0
- package/app-constructor/prompts.js +0 -120
- package/chat-copilot.js +0 -770
package/actions/generate-page.js
CHANGED
|
@@ -65,13 +65,17 @@ class GeneratePage {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
static async system_prompt() {
|
|
68
|
-
return `If the user asks to generate a page, a web page or a landing page, use the generate_page to generate a page
|
|
68
|
+
return `If the user asks to generate a page, a web page or a landing page, use the generate_page to generate a page.
|
|
69
|
+
|
|
70
|
+
Critical: When generating HTML, your response must be the complete, final HTML document and nothing else. Do not include reasoning, planning notes, explanatory text, TODO comments, markdown code fences, or placeholder content. Everything you write will be stored verbatim and rendered directly to users — there is no review step between your output and the live page.`;
|
|
69
71
|
}
|
|
70
72
|
static async follow_on_generate({ name, page_type }) {
|
|
71
73
|
if (page_type === "Marketing page") {
|
|
72
74
|
return {
|
|
73
75
|
prompt:
|
|
74
|
-
"Generate the HTML for the web page using the Bootstrap 5 CSS framework."
|
|
76
|
+
"Generate the HTML for the web page using the Bootstrap 5 CSS framework. " +
|
|
77
|
+
'Include a toast notification area just before the closing </body> tag: ' +
|
|
78
|
+
'<div id="toasts-area" class="toast-container position-fixed top-0 start-50 p-0" style="z-index:999;" aria-live="polite" aria-atomic="true"></div>',
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
const prompt = `Now generate the contents of the ${name} page`;
|
|
@@ -156,6 +156,11 @@ class GenerateTables {
|
|
|
156
156
|
type: "string",
|
|
157
157
|
description: "The name of the table",
|
|
158
158
|
},
|
|
159
|
+
description: {
|
|
160
|
+
type: "string",
|
|
161
|
+
description:
|
|
162
|
+
"A short human-readable description of what this table stores and its role in the application",
|
|
163
|
+
},
|
|
159
164
|
fields: {
|
|
160
165
|
type: "array",
|
|
161
166
|
items: this.field_item_schema(),
|
|
@@ -163,6 +168,13 @@ class GenerateTables {
|
|
|
163
168
|
},
|
|
164
169
|
},
|
|
165
170
|
},
|
|
171
|
+
reused_table_names: {
|
|
172
|
+
type: "array",
|
|
173
|
+
items: { type: "string" },
|
|
174
|
+
description:
|
|
175
|
+
"Names of existing tables that are already complete and require no changes. " +
|
|
176
|
+
"List them here so the caller knows which tables were reused as-is. Do NOT repeat their field definitions in the tables array.",
|
|
177
|
+
},
|
|
166
178
|
},
|
|
167
179
|
};
|
|
168
180
|
}
|
|
@@ -202,6 +214,12 @@ class GenerateTables {
|
|
|
202
214
|
want to add or update. The system will automatically add new fields and update the settings of
|
|
203
215
|
existing fields — it will not recreate or drop the table.
|
|
204
216
|
|
|
217
|
+
## Reused tables
|
|
218
|
+
|
|
219
|
+
If an existing table is used by the application as-is (no new fields needed), do NOT repeat it
|
|
220
|
+
in the tables array. Instead, add its name to the reused_table_names array. This tells the
|
|
221
|
+
system to include it in the schema diagram without attempting to modify it.
|
|
222
|
+
|
|
205
223
|
If a user requests creating a table with certain fields and the table already exists, automatically add any missing fields to that table. Do not ask the user for confirmation or prompt them again—just proceed with the table update.
|
|
206
224
|
|
|
207
225
|
If a table has a ForeignKey field that references another table which does not yet exist in the
|
|
@@ -250,6 +268,28 @@ class GenerateTables {
|
|
|
250
268
|
The type_and_configuration.data_type for a calculated field should reflect the return type of
|
|
251
269
|
the expression (e.g. Integer, Float, String, Bool).
|
|
252
270
|
|
|
271
|
+
## Table descriptions
|
|
272
|
+
|
|
273
|
+
Every table you define MUST include a description — a short sentence explaining what
|
|
274
|
+
the table stores and its role in the application. This description is shown in
|
|
275
|
+
subsequent prompts to give the planner context about the schema, so make it
|
|
276
|
+
informative (e.g. "Stores billable time entries logged by lawyers against a project").
|
|
277
|
+
|
|
278
|
+
## Bulk data import and export
|
|
279
|
+
|
|
280
|
+
Do NOT create tables whose purpose is to trigger a bulk import or export (e.g. a
|
|
281
|
+
table with a File field that a user fills in via an Edit view to start an import).
|
|
282
|
+
Bulk import and export is a UI concern — there are plugins that provide dedicated
|
|
283
|
+
viewtemplates operating directly on the target table, which is a much better
|
|
284
|
+
solution than a workaround table with a file field and an Edit view.
|
|
285
|
+
|
|
286
|
+
A tracking table that records the status and outcome of an automated import or
|
|
287
|
+
export process (e.g. import_jobs) is acceptable, but only if the table is populated
|
|
288
|
+
automatically by the process — not filled in manually by a user. Such tables must
|
|
289
|
+
have a description that clearly says they are auto-populated and must not be edited
|
|
290
|
+
by hand (e.g. "Auto-populated by the import process. Records status and errors for
|
|
291
|
+
each import run. Not editable by users.").
|
|
292
|
+
|
|
253
293
|
## Existing tables
|
|
254
294
|
|
|
255
295
|
The database already contains the following tables:
|
|
@@ -279,8 +319,10 @@ class GenerateTables {
|
|
|
279
319
|
}
|
|
280
320
|
|
|
281
321
|
static async execute({ tables }, req) {
|
|
282
|
-
const
|
|
283
|
-
|
|
322
|
+
const existingDbTables = await Table.find({});
|
|
323
|
+
const sctables = this.process_tables(tables, existingDbTables);
|
|
324
|
+
for (const table of sctables)
|
|
325
|
+
await Table.create(table.name, { description: table.description || "" });
|
|
284
326
|
for (const table of sctables) {
|
|
285
327
|
for (const field of table.fields) {
|
|
286
328
|
field.table = Table.findOne({ name: table.name });
|
|
@@ -309,10 +351,11 @@ class GenerateTables {
|
|
|
309
351
|
const added = [];
|
|
310
352
|
const updated = [];
|
|
311
353
|
|
|
354
|
+
const existingDbTables = await Table.find({});
|
|
312
355
|
for (const f of sanitized) {
|
|
313
356
|
const fname = (f?.name || "").toLowerCase();
|
|
314
357
|
if (!fname) continue;
|
|
315
|
-
const processed = this.process_field(f, []);
|
|
358
|
+
const processed = this.process_field(f, [], existingDbTables);
|
|
316
359
|
const existing = existingFieldMap.get(fname);
|
|
317
360
|
if (!existing) {
|
|
318
361
|
processed.table = table;
|
|
@@ -346,7 +389,7 @@ class GenerateTables {
|
|
|
346
389
|
return { added, updated };
|
|
347
390
|
}
|
|
348
391
|
|
|
349
|
-
static process_field(f, allTablesList = []) {
|
|
392
|
+
static process_field(f, allTablesList = [], dbTables = []) {
|
|
350
393
|
if (f.aggregation) {
|
|
351
394
|
const { data_type } = f.type_and_configuration || {
|
|
352
395
|
data_type: "Integer",
|
|
@@ -374,6 +417,8 @@ class GenerateTables {
|
|
|
374
417
|
f.type_and_configuration || { data_type: "String" };
|
|
375
418
|
let type = data_type;
|
|
376
419
|
const scattributes = { ...attributes, importance: f.importance };
|
|
420
|
+
if (!scattributes.min_length) delete scattributes.min_length;
|
|
421
|
+
if (!scattributes.max_length) delete scattributes.max_length;
|
|
377
422
|
if (data_type === "ForeignKey") {
|
|
378
423
|
type = `Key to ${reference_table}`;
|
|
379
424
|
const refTableHere = allTablesList.find(
|
|
@@ -391,6 +436,21 @@ class GenerateTables {
|
|
|
391
436
|
}
|
|
392
437
|
} else if (reference_table === "users") {
|
|
393
438
|
scattributes.summary_field = "email";
|
|
439
|
+
} else {
|
|
440
|
+
const dbTable = dbTables.find((t) => t.name === reference_table);
|
|
441
|
+
if (dbTable) {
|
|
442
|
+
const strFields = (dbTable.fields || []).filter(
|
|
443
|
+
(rf) => rf.type?.name === "String" || rf.type === "String"
|
|
444
|
+
);
|
|
445
|
+
if (strFields.length) {
|
|
446
|
+
const maxImp = strFields.reduce((prev, curr) => {
|
|
447
|
+
const pi = prev?.attributes?.importance ?? 0;
|
|
448
|
+
const ci = curr?.attributes?.importance ?? 0;
|
|
449
|
+
return pi >= ci ? prev : curr;
|
|
450
|
+
});
|
|
451
|
+
scattributes.summary_field = maxImp.name;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
394
454
|
}
|
|
395
455
|
}
|
|
396
456
|
return {
|
|
@@ -404,14 +464,17 @@ class GenerateTables {
|
|
|
404
464
|
};
|
|
405
465
|
}
|
|
406
466
|
|
|
407
|
-
static process_tables(tables) {
|
|
467
|
+
static process_tables(tables, dbTables = []) {
|
|
408
468
|
return tables.map((table) => {
|
|
409
469
|
const sanitizedFields = Array.isArray(table.fields)
|
|
410
470
|
? table.fields.filter((f) => (f?.name || "").toLowerCase() !== "id")
|
|
411
471
|
: [];
|
|
412
472
|
return new Table({
|
|
413
473
|
name: table.table_name,
|
|
414
|
-
|
|
474
|
+
description: table.description || "",
|
|
475
|
+
fields: sanitizedFields.map((f) =>
|
|
476
|
+
this.process_field(f, tables, dbTables)
|
|
477
|
+
),
|
|
415
478
|
});
|
|
416
479
|
});
|
|
417
480
|
}
|
|
@@ -451,6 +514,7 @@ const buildMermaidMarkup = (tables) => {
|
|
|
451
514
|
};
|
|
452
515
|
|
|
453
516
|
module.exports = GenerateTables;
|
|
517
|
+
module.exports.buildMermaidMarkup = buildMermaidMarkup;
|
|
454
518
|
|
|
455
519
|
/* todo
|
|
456
520
|
|
|
@@ -272,6 +272,16 @@ class GenerateWorkflow {
|
|
|
272
272
|
IMPORTANT — keep row expressions simple; use dedicated steps for data fetching:
|
|
273
273
|
Row expressions (e.g. in modify_row) should be simple references to values already in the context — not inline queries or complex logic. If the values you need are not already in context, add a dedicated step before the row expression step to fetch or compute them and write the results into the context. Choose the step type that fits the job: TableQuery to query a table, run_js_code for custom computation, or any other appropriate step. Each step should do one clear thing; the row expression then just picks the relevant context values.
|
|
274
274
|
|
|
275
|
+
CRITICAL — modify_row without a triggering row requires where="Database" + select_table + query (all three):
|
|
276
|
+
A basic modify_row step updates the row that triggered the workflow. If the workflow is NOT triggered by a specific table row (e.g. triggered by a button with no table, or running inside a ForLoop), there is no triggering row and table is undefined — this crashes with "Cannot read properties of undefined (reading 'tryUpdateRow')".
|
|
277
|
+
To update arbitrary rows in a workflow, the step configuration MUST include all three of:
|
|
278
|
+
- where: "Database"
|
|
279
|
+
- select_table: the name of the table to update (string)
|
|
280
|
+
- query: a JS expression (evaluated against the workflow context) returning a where-clause object that identifies the row(s) to update — e.g. {id: bh_id}
|
|
281
|
+
If any of these three is missing, the action falls through to the triggering-row path and crashes when table is undefined.
|
|
282
|
+
Example modify_row step configuration for updating a billable_hours row inside a ForLoop where bh_id is in context:
|
|
283
|
+
{ "step_type": "modify_row", "where": "Database", "select_table": "billable_hours", "query": "{id: bh_id}", "row_expr": "{invoiced: true}" }
|
|
284
|
+
|
|
275
285
|
CRITICAL — every workflow must form a single connected chain from the first step to the last:
|
|
276
286
|
- Every step except the very last one MUST have a next_step that names another step in the workflow.
|
|
277
287
|
- The very last step must have next_step omitted or set to an empty string to terminate the workflow.
|
|
@@ -282,6 +292,20 @@ class GenerateWorkflow {
|
|
|
282
292
|
steps can read values from and write values to. This context is a state that is persisted on disk for each workflow
|
|
283
293
|
run.
|
|
284
294
|
|
|
295
|
+
Important: every {{}} interpolation in any workflow step (email body, email subject, filename, prompt template,
|
|
296
|
+
etc.) must reference a variable that already exists in the workflow context at the point the step runs.
|
|
297
|
+
Do NOT use fallback expressions such as {{invoice_date || new Date().toISOString()}} — if the variable is not
|
|
298
|
+
defined, the interpolation engine throws before the || fallback can execute. For example, a send_email step
|
|
299
|
+
using {{invoice_date}} will fail with "invoice_date is not defined" if no prior step put invoice_date in context.
|
|
300
|
+
If a value might not be in context at that point, retrieve or compute it in an earlier step and store it under
|
|
301
|
+
a known key.
|
|
302
|
+
|
|
303
|
+
Important: when a workflow is triggered from a Show view action button, the trigger must have its table set to
|
|
304
|
+
the view's table. Saltcorn then automatically passes the full row as the initial workflow context — all field
|
|
305
|
+
values are available by their field names (e.g. id, name, contact_email). Do NOT attempt to pass row data
|
|
306
|
+
through a state property on the actions array — it is silently ignored. If the trigger has no table set, the
|
|
307
|
+
workflow starts with an empty context and all field references will throw "is not defined".
|
|
308
|
+
|
|
285
309
|
Each step can have a next_step key which is the name of the next step, or a JavaScript expression which evaluates
|
|
286
310
|
to the name of the next step based on the context. In the evaluation of the next step, each value in the context is
|
|
287
311
|
in scope and can be addressed directly. Identifiers for the step names are also in scope, the name of the next step
|
|
@@ -307,16 +331,43 @@ class GenerateWorkflow {
|
|
|
307
331
|
Most of them are are explained by their parameter descriptions. Here are some additional information for some
|
|
308
332
|
step types:
|
|
309
333
|
|
|
334
|
+
TableQuery: stores the query results in the context under the name given in the query_variable configuration field.
|
|
335
|
+
This name is chosen by you — pick a short descriptive name (e.g. "billable_hours", "lawyer_rows").
|
|
336
|
+
Every subsequent step that reads those results — whether a run_js_code step or another TableQuery's query_object
|
|
337
|
+
expression — must use this exact variable name. Mismatched names cause "X is not defined" errors at runtime.
|
|
338
|
+
|
|
339
|
+
Important: an insert step writes ONLY the new row's id into the context (e.g. new_invoice_id). It does NOT
|
|
340
|
+
write any other field values from the inserted row. If a later step needs fields from that row (e.g. invoice_date,
|
|
341
|
+
total_amount, contact_email), add a TableQuery step immediately after the insert to fetch the row by id and make
|
|
342
|
+
its fields available in context. For example, query {id: new_invoice_id} on the invoices table with
|
|
343
|
+
query_variable "invoice_row", then reference invoice_row.invoice_date, invoice_row.total_amount etc. in
|
|
344
|
+
subsequent steps. Never assume that insert fields are in context — only the id is guaranteed.
|
|
345
|
+
|
|
310
346
|
run_js_code: if the step_type is "run_js_code" then the step object should include the JavaScript code to be executed in the "code"
|
|
311
347
|
key. You can use await in the code if you need to run asynchronous code. The values in the context are directly in scope and can be accessed using their name. In addition, the variable
|
|
312
348
|
"context" is also in scope and can be used to address the context as a whole. To write values to the context, return an
|
|
313
349
|
object. The following Saltcorn models are already available in scope without any require: Table, Row, Field, User, View, Trigger, Page, File — use them directly.
|
|
314
350
|
When you need to require other modules, always use a plain require call, e.g. const moment = require('moment');
|
|
315
|
-
NEVER use the pattern const X = X || require(...) — this causes a ReferenceError because const variables cannot be referenced before initialization.
|
|
351
|
+
NEVER use the pattern const X = X || require(...) — this causes a ReferenceError because const variables cannot be referenced before initialization.
|
|
352
|
+
The values in this object will be written into the current context. If a value already exists in the context
|
|
316
353
|
it will be overwritten. For example, If the context contains values x and y which are numbers and you would like to push
|
|
317
|
-
the value "sum" which is the sum of x and y, then use this as the code: return {sum: x+y}. You cannot set the next step in the
|
|
354
|
+
the value "sum" which is the sum of x and y, then use this as the code: return {sum: x+y}. You cannot set the next step in the
|
|
318
355
|
return object or by returning a string from a run_js_code step, this will not work. To set the next step from a code action, always use the next_step property of the step object.
|
|
319
|
-
This expression for the next step can depend on value pushed to the context (by the return object in the code) as these values are in scope.
|
|
356
|
+
This expression for the next step can depend on value pushed to the context (by the return object in the code) as these values are in scope.
|
|
357
|
+
|
|
358
|
+
SetContext: sets one or more variables in the workflow context. The configuration must include a ctx_values key
|
|
359
|
+
whose value is a JavaScript object expression — it MUST be wrapped in curly braces.
|
|
360
|
+
Example: {running_total: 0} initialises a counter, {invoice_date: new Date().toISOString().slice(0,10)} sets a date.
|
|
361
|
+
Do NOT write bare key-value pairs such as running_total: 0 — without the braces the expression is invalid and
|
|
362
|
+
throws "running_total is not defined" when a later step tries to read it.
|
|
363
|
+
The object expression can reference existing context variables: {total: subtotal + tax}.
|
|
364
|
+
|
|
365
|
+
Early termination pattern: to stop a workflow when a condition is not met (e.g. no rows found),
|
|
366
|
+
use a run_js_code step that writes a boolean flag to the context (e.g. return {has_rows: billable_hours.length > 0}),
|
|
367
|
+
then set that step's next_step to a conditional expression (e.g. has_rows ? "next_real_step" : "abort_step"),
|
|
368
|
+
and add a TerminateWorkflow step named "abort_step".
|
|
369
|
+
Do NOT try to terminate from inside run_js_code by returning null, throwing, or omitting a return — those do not stop the workflow.
|
|
370
|
+
The conditional must be in the step's next_step field, not inside the code.
|
|
320
371
|
|
|
321
372
|
ForLoop: ForLoop steps loop over an array which is specified by the array_expression JavaScript expression. Execution of the workflow steps is temporarily diverted to another set
|
|
322
373
|
of steps, starting from the step specified by the loop_body_inital_step value, and runs until it encounters a
|
package/agent-skills/pagegen.js
CHANGED
|
@@ -52,22 +52,59 @@ class GeneratePageSkill {
|
|
|
52
52
|
}
|
|
53
53
|
get userActions() {
|
|
54
54
|
return {
|
|
55
|
-
async build_copilot_page_gen({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
55
|
+
async build_copilot_page_gen({
|
|
56
|
+
user,
|
|
57
|
+
name,
|
|
58
|
+
title,
|
|
59
|
+
description,
|
|
60
|
+
html,
|
|
61
|
+
min_role = 100,
|
|
62
|
+
}) {
|
|
63
|
+
const existingPage = Page.findOne({ name });
|
|
64
|
+
if (existingPage) {
|
|
65
|
+
// Overwrite existing file if the page already has one, otherwise create new
|
|
66
|
+
let filePath;
|
|
67
|
+
if (existingPage.layout?.html_file) {
|
|
68
|
+
const existingFile = await File.findOne(
|
|
69
|
+
existingPage.layout.html_file
|
|
70
|
+
);
|
|
71
|
+
if (existingFile) {
|
|
72
|
+
await existingFile.overwrite_contents(html);
|
|
73
|
+
filePath = existingFile.path_to_serve;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!filePath) {
|
|
77
|
+
const file = await File.from_contents(
|
|
78
|
+
`${name}.html`,
|
|
79
|
+
"text/html",
|
|
80
|
+
html,
|
|
81
|
+
user?.id,
|
|
82
|
+
min_role
|
|
83
|
+
);
|
|
84
|
+
filePath = file.path_to_serve;
|
|
85
|
+
}
|
|
86
|
+
await Page.update(existingPage.id, {
|
|
87
|
+
title,
|
|
88
|
+
description,
|
|
89
|
+
min_role,
|
|
90
|
+
layout: { html_file: filePath },
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
const file = await File.from_contents(
|
|
94
|
+
`${name}.html`,
|
|
95
|
+
"text/html",
|
|
96
|
+
html,
|
|
97
|
+
user?.id,
|
|
98
|
+
min_role
|
|
99
|
+
);
|
|
100
|
+
await Page.create({
|
|
101
|
+
name,
|
|
102
|
+
title,
|
|
103
|
+
description,
|
|
104
|
+
min_role,
|
|
105
|
+
layout: { html_file: file.path_to_serve },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
71
108
|
setTimeout(() => getState().refresh_pages(), 200);
|
|
72
109
|
return {
|
|
73
110
|
notify: `Page saved: <a target="_blank" href="/page/${name}">${name}</a>`,
|
|
@@ -97,61 +134,130 @@ class GeneratePageSkill {
|
|
|
97
134
|
} else return "Metadata recieved";
|
|
98
135
|
},
|
|
99
136
|
postProcess: async ({ tool_call, generate, req }) => {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
const version_tag = db.connectObj.version_tag;
|
|
138
|
+
const asset = (name) => `/static_assets/${version_tag}/${name}`;
|
|
139
|
+
const { name, title, description, min_role, page_type } =
|
|
140
|
+
tool_call.input;
|
|
141
|
+
|
|
142
|
+
if (page_type === "Layout page") {
|
|
143
|
+
const str = await generate(
|
|
144
|
+
`Generate the Saltcorn layout JSON for the ${name} page. ` +
|
|
145
|
+
`Your response must be a single valid JSON object — no explanatory text, no markdown fences, no reasoning. ` +
|
|
146
|
+
`\n\nThe layout is a tree of segments. The top-level object is:\n` +
|
|
147
|
+
` {"above": [...segments...]} — stack items vertically\n` +
|
|
148
|
+
`or:\n` +
|
|
149
|
+
` {"besides": [...segments...], "widths": [6,6]} — place side by side (widths must sum to 12)\n` +
|
|
150
|
+
`\nAvailable segment types:\n` +
|
|
151
|
+
`- Embed a view (receives page URL state, e.g. id): {"type":"view","view":"viewname","state":"shared"}\n` +
|
|
152
|
+
` Use state "shared" whenever the embedded view needs variables from the page URL (such as id for a Show view).\n` +
|
|
153
|
+
` To pass only specific variables, add extra_state_fml: {"type":"view","view":"viewname","state":"shared","extra_state_fml":"{id: id}"}\n` +
|
|
154
|
+
`- HTML block: {"type":"blank","isHTML":true,"contents":"<p>html</p>"}\n` +
|
|
155
|
+
`- Text block: {"type":"blank","contents":"plain text"}\n` +
|
|
156
|
+
`- Container/wrapper: {"type":"container","customClass":"p-3","htmlElement":"div","contents":{segment}}\n` +
|
|
157
|
+
`- Vertical stack: {"above":[{segment},{segment}]}\n` +
|
|
158
|
+
`- Horizontal split: {"besides":[{segment},{segment}],"widths":[8,4]}\n`
|
|
159
|
+
);
|
|
160
|
+
const jsonStr = str.includes("```json")
|
|
161
|
+
? str.split("```json")[1].split("```")[0]
|
|
162
|
+
: str.includes("```")
|
|
163
|
+
? str.split("```")[1].split("```")[0]
|
|
164
|
+
: str;
|
|
165
|
+
let layout;
|
|
166
|
+
try {
|
|
167
|
+
layout = JSON.parse(jsonStr.trim());
|
|
168
|
+
} catch (e) {
|
|
169
|
+
return {
|
|
170
|
+
stop: true,
|
|
171
|
+
add_response: `Error parsing layout JSON for ${name}: ${e.message}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (this.yoloMode) {
|
|
175
|
+
const existingPage = await Page.findOne({ name });
|
|
176
|
+
if (existingPage) {
|
|
177
|
+
await Page.update(existingPage.id, {
|
|
178
|
+
title,
|
|
179
|
+
description,
|
|
180
|
+
min_role: min_role ?? 100,
|
|
181
|
+
layout,
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
await Page.create({
|
|
185
|
+
name,
|
|
186
|
+
title,
|
|
187
|
+
description,
|
|
188
|
+
min_role: min_role ?? 100,
|
|
189
|
+
layout,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
setTimeout(() => getState().refresh_pages(), 200);
|
|
193
|
+
return {
|
|
194
|
+
stop: true,
|
|
195
|
+
add_response: `Page ${name} created.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
stop: true,
|
|
200
|
+
add_response: `Page ${name} layout generated (preview not available for layout pages).`,
|
|
201
|
+
};
|
|
202
|
+
} else {
|
|
203
|
+
const str = await generate(
|
|
204
|
+
`Generate the complete HTML for the ${name} page. Your response must be the raw HTML document only — no explanatory text before or after, no markdown code fences, no reasoning, no placeholder comments. The output is saved and rendered directly to users without any review step, so anything you write outside valid HTML will be visible on the page.
|
|
205
|
+
|
|
206
|
+
If I asked you to embed a view, use the <embed-view> self-closing tag to do so, setting the view name in the viewname attribute. For example,
|
|
103
207
|
to embed the view LeadForm inside a div, write: <div><embed-view viewname="LeadForm"></div>
|
|
104
|
-
|
|
208
|
+
|
|
105
209
|
If you need to include the standard bootstrap CSS and javascript files, they are available as:
|
|
106
210
|
|
|
107
211
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
|
108
212
|
|
|
109
|
-
and
|
|
110
|
-
<script src="
|
|
213
|
+
and
|
|
214
|
+
<script src="${asset("jquery-3.6.0.min.js")}"></script>
|
|
111
215
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
|
112
216
|
|
|
113
217
|
If you are embedding views with <embed-view>, you should also embed the following script sources at the end of the <body> tag to make sure the content inside those views works:
|
|
114
218
|
|
|
115
|
-
<script src="
|
|
116
|
-
<script src="
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
219
|
+
<script src="${asset("saltcorn-common.js")}"></script>
|
|
220
|
+
<script src="${asset("saltcorn.js")}"></script>
|
|
221
|
+
`
|
|
222
|
+
);
|
|
223
|
+
const html = str.includes("```html")
|
|
224
|
+
? str.split("```html")[1].split("```")[0]
|
|
225
|
+
: str;
|
|
226
|
+
|
|
227
|
+
if (this.yoloMode) {
|
|
228
|
+
await this.userActions.build_copilot_page_gen({
|
|
229
|
+
user: req?.user,
|
|
230
|
+
name,
|
|
231
|
+
title,
|
|
232
|
+
description,
|
|
233
|
+
min_role: min_role ?? 100,
|
|
234
|
+
html,
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
stop: true,
|
|
238
|
+
add_response: `Page ${name} created.`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
131
241
|
return {
|
|
132
242
|
stop: true,
|
|
133
|
-
add_response:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
srcdoc: text_attr(html),
|
|
140
|
-
width: 500,
|
|
141
|
-
height: 800,
|
|
142
|
-
}),
|
|
143
|
-
add_system_prompt: `If the user asks you to regenerate the page,
|
|
243
|
+
add_response: iframe({
|
|
244
|
+
srcdoc: text_attr(html),
|
|
245
|
+
width: 500,
|
|
246
|
+
height: 800,
|
|
247
|
+
}),
|
|
248
|
+
add_system_prompt: `If the user asks you to regenerate the page,
|
|
144
249
|
you must run the generate_page tool again. After running this tool
|
|
145
250
|
you will be prompted to generate the html again. You should repeat
|
|
146
251
|
the html from the previous answer except for the changes the user
|
|
147
252
|
is requesting.`,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
253
|
+
add_user_action: {
|
|
254
|
+
name: "build_copilot_page_gen",
|
|
255
|
+
type: "button",
|
|
256
|
+
label: "Save page " + name,
|
|
257
|
+
input: { html },
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
155
261
|
},
|
|
156
262
|
|
|
157
263
|
/*renderToolCall({ phrase }, { req }) {
|
|
@@ -163,7 +269,7 @@ class GeneratePageSkill {
|
|
|
163
269
|
response.includes("Unable to provide HTML for this page")
|
|
164
270
|
)
|
|
165
271
|
return response;
|
|
166
|
-
|
|
272
|
+
if (
|
|
167
273
|
typeof response === "string" &&
|
|
168
274
|
response.includes("The HTML code for the ")
|
|
169
275
|
)
|
|
@@ -194,11 +300,17 @@ class GeneratePageSkill {
|
|
|
194
300
|
"A longer description that is not visible but appears in the page header and is indexed by search engines",
|
|
195
301
|
type: "string",
|
|
196
302
|
},
|
|
303
|
+
min_role: {
|
|
304
|
+
description:
|
|
305
|
+
"Minimum role required to access this page. Use 1 for admin-only, 40 for staff and above, 80 for logged-in users and above, 100 for public. Set this based on the intended audience described in the task.",
|
|
306
|
+
type: "integer",
|
|
307
|
+
enum: [1, 40, 80, 100],
|
|
308
|
+
},
|
|
197
309
|
page_type: {
|
|
198
310
|
description:
|
|
199
|
-
"The type of page to generate
|
|
311
|
+
"The type of page to generate. Use 'Layout page' for any page that is part of the application (dashboards, print pages, data pages, pages that embed views) — this creates a proper Saltcorn layout page. Use 'Marketing page' for public-facing promotional pages such as landing pages or brochures. Use 'Application page' only for standalone HTML pages that are part of the app but do not embed Saltcorn views.",
|
|
200
312
|
type: "string",
|
|
201
|
-
enum: ["Marketing page", "Application page"],
|
|
313
|
+
enum: ["Layout page", "Marketing page", "Application page"],
|
|
202
314
|
},
|
|
203
315
|
},
|
|
204
316
|
},
|
|
@@ -6,6 +6,7 @@ const Field = require("@saltcorn/data/models/field");
|
|
|
6
6
|
const User = require("@saltcorn/data/models/user");
|
|
7
7
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
8
8
|
const Role = require("@saltcorn/data/models/role");
|
|
9
|
+
const File = require("@saltcorn/data/models/file");
|
|
9
10
|
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
10
11
|
const { getState } = require("@saltcorn/data/db/state");
|
|
11
12
|
|
|
@@ -167,6 +168,7 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
167
168
|
"trigger",
|
|
168
169
|
"plugin",
|
|
169
170
|
"system-configuration-value",
|
|
171
|
+
"file",
|
|
170
172
|
"type",
|
|
171
173
|
],
|
|
172
174
|
},
|
|
@@ -317,6 +319,11 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
317
319
|
}
|
|
318
320
|
return v;
|
|
319
321
|
}
|
|
322
|
+
case "file": {
|
|
323
|
+
const file = await File.findOne({ filename: input.entity_name });
|
|
324
|
+
if (!file) return `file not found`;
|
|
325
|
+
return { filename: file.filename, min_role_read: file.min_role_read };
|
|
326
|
+
}
|
|
320
327
|
case "plugin": {
|
|
321
328
|
const plugin = await Plugin.findOne({ name: input.entity_name });
|
|
322
329
|
if (!plugin) return `plugin not found`;
|
|
@@ -369,6 +376,18 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
369
376
|
const trigger = Trigger.findOne({ name: input.entity_name });
|
|
370
377
|
if (!trigger) return `trigger not found`;
|
|
371
378
|
const value = trigger.toJson;
|
|
379
|
+
// For workflow triggers, attach all steps so the agent can see their configurations
|
|
380
|
+
if (trigger.action === "Workflow") {
|
|
381
|
+
const steps = await WorkflowStep.find({ trigger_id: trigger.id });
|
|
382
|
+
value.steps = steps.map((s) => ({
|
|
383
|
+
id: s.id,
|
|
384
|
+
name: s.name,
|
|
385
|
+
action_name: s.action_name,
|
|
386
|
+
initial_step: s.initial_step,
|
|
387
|
+
next_step: s.next_step,
|
|
388
|
+
configuration: s.configuration,
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
372
391
|
const schema = {
|
|
373
392
|
name: { type: "string", description: "trigger name" },
|
|
374
393
|
action: {
|
|
@@ -473,6 +492,7 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
473
492
|
"system-configuration-value",
|
|
474
493
|
"module-configuration",
|
|
475
494
|
"role",
|
|
495
|
+
"file",
|
|
476
496
|
],
|
|
477
497
|
},
|
|
478
498
|
entity_name: {
|
|
@@ -718,8 +738,9 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
718
738
|
table || table_name,
|
|
719
739
|
)?.id;
|
|
720
740
|
|
|
721
|
-
// Pre-check action before saving
|
|
722
|
-
|
|
741
|
+
// Pre-check action before saving (skip built-ins handled outside actions registry)
|
|
742
|
+
const builtinActions = new Set(["Workflow", "Multi-step action"]);
|
|
743
|
+
if (tsNoTableName.action && !builtinActions.has(tsNoTableName.action)) {
|
|
723
744
|
const action = getState().actions[tsNoTableName.action];
|
|
724
745
|
if (!action)
|
|
725
746
|
return `Action '${tsNoTableName.action}' not found`;
|
|
@@ -897,6 +918,13 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
897
918
|
await getState().refresh_roles();
|
|
898
919
|
return "Role created";
|
|
899
920
|
}
|
|
921
|
+
|
|
922
|
+
case "file": {
|
|
923
|
+
const file = await File.findOne({ filename: input.entity_name });
|
|
924
|
+
if (!file) return `file not found: ${input.entity_name}`;
|
|
925
|
+
await file.set_role(entityValue.min_role_read);
|
|
926
|
+
return "Done";
|
|
927
|
+
}
|
|
900
928
|
}
|
|
901
929
|
return "Done";
|
|
902
930
|
} catch (e) {
|
|
@@ -33,11 +33,7 @@ class AnyActionSkill {
|
|
|
33
33
|
`If the task requires several independent single-step actions (e.g. "mark complete" and "mark incomplete"), call this tool once per action — do NOT bundle them into one workflow.\n\n` +
|
|
34
34
|
`**Navigation is a view concern:** If a task description says "return the user to X" or "navigate back", ` +
|
|
35
35
|
`do NOT add a navigation step inside the trigger. Triggers only handle data operations. ` +
|
|
36
|
-
`Navigation (GoBack) is configured on the button in the list view, not inside the trigger itself
|
|
37
|
-
`**Verification/confirmation emails after user registration:** Create a trigger with event "Insert" on the "users" table, ` +
|
|
38
|
-
`action_type "run_js_code". Do NOT use the send_email action — it will not work for verification. ` +
|
|
39
|
-
`Set the action_config "code" field to exactly:\n` +
|
|
40
|
-
`const { send_verification_email } = require("@saltcorn/data/models/email");\nawait send_verification_email(row, req);`
|
|
36
|
+
`Navigation (GoBack) is configured on the button in the list view, not inside the trigger itself.`
|
|
41
37
|
);
|
|
42
38
|
}
|
|
43
39
|
|