@saltcorn/copilot 0.8.2 → 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 +56 -6
- package/actions/generate-workflow.js +54 -3
- package/agent-skills/pagegen.js +155 -57
- package/agent-skills/registry-editor.js +15 -2
- 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 +113 -54
- package/app-constructor/research.js +350 -0
- package/app-constructor/run_task.js +195 -64
- package/app-constructor/schema.js +163 -324
- package/app-constructor/tasks.js +90 -886
- package/app-constructor/tools.js +13 -1
- package/app-constructor/view.js +307 -54
- package/builder-gen.js +11 -8
- package/builder-schema.js +6 -0
- package/package.json +1 -1
- package/app-constructor/prompts.js +0 -120
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(),
|
|
@@ -263,6 +268,28 @@ class GenerateTables {
|
|
|
263
268
|
The type_and_configuration.data_type for a calculated field should reflect the return type of
|
|
264
269
|
the expression (e.g. Integer, Float, String, Bool).
|
|
265
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
|
+
|
|
266
293
|
## Existing tables
|
|
267
294
|
|
|
268
295
|
The database already contains the following tables:
|
|
@@ -292,8 +319,10 @@ class GenerateTables {
|
|
|
292
319
|
}
|
|
293
320
|
|
|
294
321
|
static async execute({ tables }, req) {
|
|
295
|
-
const
|
|
296
|
-
|
|
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 || "" });
|
|
297
326
|
for (const table of sctables) {
|
|
298
327
|
for (const field of table.fields) {
|
|
299
328
|
field.table = Table.findOne({ name: table.name });
|
|
@@ -322,10 +351,11 @@ class GenerateTables {
|
|
|
322
351
|
const added = [];
|
|
323
352
|
const updated = [];
|
|
324
353
|
|
|
354
|
+
const existingDbTables = await Table.find({});
|
|
325
355
|
for (const f of sanitized) {
|
|
326
356
|
const fname = (f?.name || "").toLowerCase();
|
|
327
357
|
if (!fname) continue;
|
|
328
|
-
const processed = this.process_field(f, []);
|
|
358
|
+
const processed = this.process_field(f, [], existingDbTables);
|
|
329
359
|
const existing = existingFieldMap.get(fname);
|
|
330
360
|
if (!existing) {
|
|
331
361
|
processed.table = table;
|
|
@@ -359,7 +389,7 @@ class GenerateTables {
|
|
|
359
389
|
return { added, updated };
|
|
360
390
|
}
|
|
361
391
|
|
|
362
|
-
static process_field(f, allTablesList = []) {
|
|
392
|
+
static process_field(f, allTablesList = [], dbTables = []) {
|
|
363
393
|
if (f.aggregation) {
|
|
364
394
|
const { data_type } = f.type_and_configuration || {
|
|
365
395
|
data_type: "Integer",
|
|
@@ -387,6 +417,8 @@ class GenerateTables {
|
|
|
387
417
|
f.type_and_configuration || { data_type: "String" };
|
|
388
418
|
let type = data_type;
|
|
389
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;
|
|
390
422
|
if (data_type === "ForeignKey") {
|
|
391
423
|
type = `Key to ${reference_table}`;
|
|
392
424
|
const refTableHere = allTablesList.find(
|
|
@@ -404,6 +436,21 @@ class GenerateTables {
|
|
|
404
436
|
}
|
|
405
437
|
} else if (reference_table === "users") {
|
|
406
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
|
+
}
|
|
407
454
|
}
|
|
408
455
|
}
|
|
409
456
|
return {
|
|
@@ -417,14 +464,17 @@ class GenerateTables {
|
|
|
417
464
|
};
|
|
418
465
|
}
|
|
419
466
|
|
|
420
|
-
static process_tables(tables) {
|
|
467
|
+
static process_tables(tables, dbTables = []) {
|
|
421
468
|
return tables.map((table) => {
|
|
422
469
|
const sanitizedFields = Array.isArray(table.fields)
|
|
423
470
|
? table.fields.filter((f) => (f?.name || "").toLowerCase() !== "id")
|
|
424
471
|
: [];
|
|
425
472
|
return new Table({
|
|
426
473
|
name: table.table_name,
|
|
427
|
-
|
|
474
|
+
description: table.description || "",
|
|
475
|
+
fields: sanitizedFields.map((f) =>
|
|
476
|
+
this.process_field(f, tables, dbTables)
|
|
477
|
+
),
|
|
428
478
|
});
|
|
429
479
|
});
|
|
430
480
|
}
|
|
@@ -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
|
@@ -60,21 +60,51 @@ class GeneratePageSkill {
|
|
|
60
60
|
html,
|
|
61
61
|
min_role = 100,
|
|
62
62
|
}) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
}
|
|
78
108
|
setTimeout(() => getState().refresh_pages(), 200);
|
|
79
109
|
return {
|
|
80
110
|
notify: `Page saved: <a target="_blank" href="/page/${name}">${name}</a>`,
|
|
@@ -104,62 +134,130 @@ class GeneratePageSkill {
|
|
|
104
134
|
} else return "Metadata recieved";
|
|
105
135
|
},
|
|
106
136
|
postProcess: async ({ tool_call, generate, req }) => {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
110
207
|
to embed the view LeadForm inside a div, write: <div><embed-view viewname="LeadForm"></div>
|
|
111
|
-
|
|
208
|
+
|
|
112
209
|
If you need to include the standard bootstrap CSS and javascript files, they are available as:
|
|
113
210
|
|
|
114
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">
|
|
115
212
|
|
|
116
|
-
and
|
|
117
|
-
<script src="
|
|
213
|
+
and
|
|
214
|
+
<script src="${asset("jquery-3.6.0.min.js")}"></script>
|
|
118
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>
|
|
119
216
|
|
|
120
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:
|
|
121
218
|
|
|
122
|
-
<script src="
|
|
123
|
-
<script src="
|
|
219
|
+
<script src="${asset("saltcorn-common.js")}"></script>
|
|
220
|
+
<script src="${asset("saltcorn.js")}"></script>
|
|
124
221
|
`
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
|
139
241
|
return {
|
|
140
242
|
stop: true,
|
|
141
|
-
add_response:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
srcdoc: text_attr(html),
|
|
148
|
-
width: 500,
|
|
149
|
-
height: 800,
|
|
150
|
-
}),
|
|
151
|
-
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,
|
|
152
249
|
you must run the generate_page tool again. After running this tool
|
|
153
250
|
you will be prompted to generate the html again. You should repeat
|
|
154
251
|
the html from the previous answer except for the changes the user
|
|
155
252
|
is requesting.`,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
253
|
+
add_user_action: {
|
|
254
|
+
name: "build_copilot_page_gen",
|
|
255
|
+
type: "button",
|
|
256
|
+
label: "Save page " + name,
|
|
257
|
+
input: { html },
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
163
261
|
},
|
|
164
262
|
|
|
165
263
|
/*renderToolCall({ phrase }, { req }) {
|
|
@@ -210,9 +308,9 @@ class GeneratePageSkill {
|
|
|
210
308
|
},
|
|
211
309
|
page_type: {
|
|
212
310
|
description:
|
|
213
|
-
"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.",
|
|
214
312
|
type: "string",
|
|
215
|
-
enum: ["Marketing page", "Application page"],
|
|
313
|
+
enum: ["Layout page", "Marketing page", "Application page"],
|
|
216
314
|
},
|
|
217
315
|
},
|
|
218
316
|
},
|
|
@@ -376,6 +376,18 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
376
376
|
const trigger = Trigger.findOne({ name: input.entity_name });
|
|
377
377
|
if (!trigger) return `trigger not found`;
|
|
378
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
|
+
}
|
|
379
391
|
const schema = {
|
|
380
392
|
name: { type: "string", description: "trigger name" },
|
|
381
393
|
action: {
|
|
@@ -726,8 +738,9 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
726
738
|
table || table_name,
|
|
727
739
|
)?.id;
|
|
728
740
|
|
|
729
|
-
// Pre-check action before saving
|
|
730
|
-
|
|
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)) {
|
|
731
744
|
const action = getState().actions[tsNoTableName.action];
|
|
732
745
|
if (!action)
|
|
733
746
|
return `Action '${tsNoTableName.action}' not found`;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const viewname = "Saltcorn AppConstructor (experimental)";
|
|
2
2
|
|
|
3
|
+
const TaskType = Object.freeze({
|
|
4
|
+
PLUGIN: "plugin",
|
|
5
|
+
DATA_MODEL: "data_model",
|
|
6
|
+
FEATURE: "feature",
|
|
7
|
+
});
|
|
8
|
+
|
|
3
9
|
const tool_choice = (tool_name) => ({
|
|
4
10
|
tool_choice: {
|
|
5
11
|
type: "function",
|
|
@@ -9,4 +15,4 @@ const tool_choice = (tool_name) => ({
|
|
|
9
15
|
},
|
|
10
16
|
});
|
|
11
17
|
|
|
12
|
-
module.exports = { viewname, tool_choice };
|
|
18
|
+
module.exports = { viewname, tool_choice, TaskType };
|