@saltcorn/copilot 0.8.1 → 0.8.2
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-tables.js +14 -0
- package/agent-skills/pagegen.js +20 -6
- package/agent-skills/registry-editor.js +15 -0
- package/agent-skills/triggergen.js +1 -5
- package/agent-skills/viewgen.js +49 -7
- package/app-constructor/requirements.js +98 -36
- package/app-constructor/run_task.js +67 -37
- package/app-constructor/schema.js +214 -49
- package/app-constructor/tasks.js +401 -46
- package/app-constructor/tools.js +5 -4
- package/app-constructor/view.js +7 -0
- package/builder-gen.js +79 -33
- 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/chat-copilot.js +0 -770
|
@@ -163,6 +163,13 @@ class GenerateTables {
|
|
|
163
163
|
},
|
|
164
164
|
},
|
|
165
165
|
},
|
|
166
|
+
reused_table_names: {
|
|
167
|
+
type: "array",
|
|
168
|
+
items: { type: "string" },
|
|
169
|
+
description:
|
|
170
|
+
"Names of existing tables that are already complete and require no changes. " +
|
|
171
|
+
"List them here so the caller knows which tables were reused as-is. Do NOT repeat their field definitions in the tables array.",
|
|
172
|
+
},
|
|
166
173
|
},
|
|
167
174
|
};
|
|
168
175
|
}
|
|
@@ -202,6 +209,12 @@ class GenerateTables {
|
|
|
202
209
|
want to add or update. The system will automatically add new fields and update the settings of
|
|
203
210
|
existing fields — it will not recreate or drop the table.
|
|
204
211
|
|
|
212
|
+
## Reused tables
|
|
213
|
+
|
|
214
|
+
If an existing table is used by the application as-is (no new fields needed), do NOT repeat it
|
|
215
|
+
in the tables array. Instead, add its name to the reused_table_names array. This tells the
|
|
216
|
+
system to include it in the schema diagram without attempting to modify it.
|
|
217
|
+
|
|
205
218
|
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
219
|
|
|
207
220
|
If a table has a ForeignKey field that references another table which does not yet exist in the
|
|
@@ -451,6 +464,7 @@ const buildMermaidMarkup = (tables) => {
|
|
|
451
464
|
};
|
|
452
465
|
|
|
453
466
|
module.exports = GenerateTables;
|
|
467
|
+
module.exports.buildMermaidMarkup = buildMermaidMarkup;
|
|
454
468
|
|
|
455
469
|
/* todo
|
|
456
470
|
|
package/agent-skills/pagegen.js
CHANGED
|
@@ -52,20 +52,27 @@ class GeneratePageSkill {
|
|
|
52
52
|
}
|
|
53
53
|
get userActions() {
|
|
54
54
|
return {
|
|
55
|
-
async build_copilot_page_gen({
|
|
55
|
+
async build_copilot_page_gen({
|
|
56
|
+
user,
|
|
57
|
+
name,
|
|
58
|
+
title,
|
|
59
|
+
description,
|
|
60
|
+
html,
|
|
61
|
+
min_role = 100,
|
|
62
|
+
}) {
|
|
56
63
|
const file = await File.from_contents(
|
|
57
64
|
`${name}.html`,
|
|
58
65
|
"text/html",
|
|
59
66
|
html,
|
|
60
67
|
user?.id,
|
|
61
|
-
|
|
68
|
+
min_role
|
|
62
69
|
);
|
|
63
70
|
|
|
64
71
|
await Page.create({
|
|
65
72
|
name,
|
|
66
73
|
title,
|
|
67
74
|
description,
|
|
68
|
-
min_role
|
|
75
|
+
min_role,
|
|
69
76
|
layout: { html_file: file.path_to_serve },
|
|
70
77
|
});
|
|
71
78
|
setTimeout(() => getState().refresh_pages(), 200);
|
|
@@ -114,7 +121,7 @@ class GeneratePageSkill {
|
|
|
114
121
|
|
|
115
122
|
<script src="/static_assets/js/saltcorn-common.js"></script>
|
|
116
123
|
<script src="/static_assets/js/saltcorn.js">
|
|
117
|
-
|
|
124
|
+
`
|
|
118
125
|
);
|
|
119
126
|
const html = str.includes("```html")
|
|
120
127
|
? str.split("```html")[1].split("```")[0]
|
|
@@ -126,6 +133,7 @@ class GeneratePageSkill {
|
|
|
126
133
|
name: tool_call.input.name,
|
|
127
134
|
title: tool_call.input.title,
|
|
128
135
|
description: tool_call.input.description,
|
|
136
|
+
min_role: tool_call.input.min_role ?? 100,
|
|
129
137
|
html,
|
|
130
138
|
});
|
|
131
139
|
return {
|
|
@@ -148,7 +156,7 @@ class GeneratePageSkill {
|
|
|
148
156
|
add_user_action: {
|
|
149
157
|
name: "build_copilot_page_gen",
|
|
150
158
|
type: "button",
|
|
151
|
-
label: "Save page "+tool_call.input.name,
|
|
159
|
+
label: "Save page " + tool_call.input.name,
|
|
152
160
|
input: { html },
|
|
153
161
|
},
|
|
154
162
|
};
|
|
@@ -163,7 +171,7 @@ class GeneratePageSkill {
|
|
|
163
171
|
response.includes("Unable to provide HTML for this page")
|
|
164
172
|
)
|
|
165
173
|
return response;
|
|
166
|
-
|
|
174
|
+
if (
|
|
167
175
|
typeof response === "string" &&
|
|
168
176
|
response.includes("The HTML code for the ")
|
|
169
177
|
)
|
|
@@ -194,6 +202,12 @@ class GeneratePageSkill {
|
|
|
194
202
|
"A longer description that is not visible but appears in the page header and is indexed by search engines",
|
|
195
203
|
type: "string",
|
|
196
204
|
},
|
|
205
|
+
min_role: {
|
|
206
|
+
description:
|
|
207
|
+
"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.",
|
|
208
|
+
type: "integer",
|
|
209
|
+
enum: [1, 40, 80, 100],
|
|
210
|
+
},
|
|
197
211
|
page_type: {
|
|
198
212
|
description:
|
|
199
213
|
"The type of page to generate: a Marketing page if for promotional purposes, such as a landing page or a brouchure, with an appealing design. An Application page is simpler and an integrated part of the application",
|
|
@@ -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`;
|
|
@@ -473,6 +480,7 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
473
480
|
"system-configuration-value",
|
|
474
481
|
"module-configuration",
|
|
475
482
|
"role",
|
|
483
|
+
"file",
|
|
476
484
|
],
|
|
477
485
|
},
|
|
478
486
|
entity_name: {
|
|
@@ -897,6 +905,13 @@ with both the entity type and name, and the new JSON definition as a string as a
|
|
|
897
905
|
await getState().refresh_roles();
|
|
898
906
|
return "Role created";
|
|
899
907
|
}
|
|
908
|
+
|
|
909
|
+
case "file": {
|
|
910
|
+
const file = await File.findOne({ filename: input.entity_name });
|
|
911
|
+
if (!file) return `file not found: ${input.entity_name}`;
|
|
912
|
+
await file.set_role(entityValue.min_role_read);
|
|
913
|
+
return "Done";
|
|
914
|
+
}
|
|
900
915
|
}
|
|
901
916
|
return "Done";
|
|
902
917
|
} 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
|
|
package/agent-skills/viewgen.js
CHANGED
|
@@ -23,6 +23,21 @@ const {
|
|
|
23
23
|
getRelationPathsForPairs,
|
|
24
24
|
} = require("../relation-paths");
|
|
25
25
|
|
|
26
|
+
const collectViewLinks = (segment, out = []) => {
|
|
27
|
+
if (!segment || typeof segment !== "object") return out;
|
|
28
|
+
if (Array.isArray(segment)) {
|
|
29
|
+
segment.forEach((s) => collectViewLinks(s, out));
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
if (segment.type === "view_link" && segment.view) out.push(segment);
|
|
33
|
+
if (segment.above) collectViewLinks(segment.above, out);
|
|
34
|
+
if (segment.besides) collectViewLinks(segment.besides, out);
|
|
35
|
+
if (segment.contents) collectViewLinks(segment.contents, out);
|
|
36
|
+
if (Array.isArray(segment.tabs))
|
|
37
|
+
segment.tabs.forEach((t) => collectViewLinks(t?.contents, out));
|
|
38
|
+
return out;
|
|
39
|
+
};
|
|
40
|
+
|
|
26
41
|
const collectLayoutFieldNames = (segment, out = new Set()) => {
|
|
27
42
|
if (!segment || typeof segment !== "object") return out;
|
|
28
43
|
if (Array.isArray(segment)) {
|
|
@@ -129,7 +144,8 @@ class GenerateViewSkill {
|
|
|
129
144
|
`(3) Write out the complete updated configuration JSON in full — every key from the existing config must be present, with only your targeted changes merged in.\n` +
|
|
130
145
|
`(4) Call apply_view_config with that complete object. NEVER call apply_view_config before step (3) is finished. NEVER call it with only the name or a partial object — the configuration field is mandatory and must be the full merged result from step (3). Calling apply_view_config without a complete configuration is an error.\n\n` +
|
|
131
146
|
`**Generating a new view that contains view_links or embedded views:**\n` +
|
|
132
|
-
`
|
|
147
|
+
`If the task or prompt mentions a viewlink, a link to another view, or a button that opens another view from a list row, that view_link column is REQUIRED — do not omit it. ` +
|
|
148
|
+
`You MUST call get_relation_paths with all source_table/target_view pairs before constructing the layout. Never skip this step when view_links are needed.\n\n` +
|
|
133
149
|
`**Embedded view segment format (for Show layouts):**\n` +
|
|
134
150
|
` { "type": "view", "view": "<viewName>", "name": "<viewName>", "relation": "<from get_relation_paths>" }\n` +
|
|
135
151
|
`Do NOT use blank text segments as placeholders — always use a real view segment with a relation string from get_relation_paths.\n\n` +
|
|
@@ -161,11 +177,16 @@ class GenerateViewSkill {
|
|
|
161
177
|
error: `View "${name}" already exists. Use get_view_config and apply_view_config to update it.`,
|
|
162
178
|
};
|
|
163
179
|
const tableRow = table ? Table.findOne({ name: table }) : null;
|
|
164
|
-
const roleName =
|
|
180
|
+
const roleName =
|
|
181
|
+
typeof min_role === "number" ? null : min_role || "public";
|
|
165
182
|
const resolvedRole =
|
|
166
183
|
typeof min_role === "number"
|
|
167
184
|
? min_role
|
|
168
|
-
: (
|
|
185
|
+
: (
|
|
186
|
+
(getState().roles || []).find((r) => r.role === roleName) || {
|
|
187
|
+
id: 100,
|
|
188
|
+
}
|
|
189
|
+
).id;
|
|
169
190
|
await View.create({
|
|
170
191
|
name,
|
|
171
192
|
viewtemplate: viewpattern,
|
|
@@ -308,6 +329,21 @@ class GenerateViewSkill {
|
|
|
308
329
|
});
|
|
309
330
|
if (baseCfg?.columns) wfctx.columns = baseCfg.columns;
|
|
310
331
|
}
|
|
332
|
+
if (viewpattern === "List" && wfctx.layout) {
|
|
333
|
+
// initial_config_all_fields never generates ViewLink columns — inject them from layout
|
|
334
|
+
const viewLinks = collectViewLinks(wfctx.layout);
|
|
335
|
+
const viewLinkColumns = viewLinks.map((seg) => ({
|
|
336
|
+
type: "ViewLink",
|
|
337
|
+
view: seg.view,
|
|
338
|
+
block: seg.block || false,
|
|
339
|
+
label: seg.view_label || "",
|
|
340
|
+
minRole: seg.minRole || 100,
|
|
341
|
+
...(seg.relation ? { relation: seg.relation } : {}),
|
|
342
|
+
isFormula: seg.isFormula || {},
|
|
343
|
+
}));
|
|
344
|
+
if (viewLinkColumns.length > 0)
|
|
345
|
+
wfctx.columns = [...(wfctx.columns || []), ...viewLinkColumns];
|
|
346
|
+
}
|
|
311
347
|
if (viewpattern === "Edit" && table) {
|
|
312
348
|
const layoutFieldNames = collectLayoutFieldNames(wfctx.layout);
|
|
313
349
|
const fields = table.fields || [];
|
|
@@ -331,7 +367,10 @@ class GenerateViewSkill {
|
|
|
331
367
|
}
|
|
332
368
|
}
|
|
333
369
|
if (usersFkColumnsToAdd.length > 0)
|
|
334
|
-
wfctx.columns = [
|
|
370
|
+
wfctx.columns = [
|
|
371
|
+
...(wfctx.columns || []),
|
|
372
|
+
...usersFkColumnsToAdd,
|
|
373
|
+
];
|
|
335
374
|
if (Object.keys(fixed).length > 0) wfctx.fixed = fixed;
|
|
336
375
|
wfctx.destination_type = "Back to referer";
|
|
337
376
|
}
|
|
@@ -398,12 +437,15 @@ class GenerateViewSkill {
|
|
|
398
437
|
for (const field of form.fields) {
|
|
399
438
|
if (prefilledFields.has(field.name)) continue;
|
|
400
439
|
//TODO showIf
|
|
440
|
+
const isShowif = field.name.endsWith("_showif");
|
|
401
441
|
properties[field.name] = {
|
|
402
442
|
description:
|
|
403
443
|
field.copilot_description ||
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
444
|
+
(isShowif
|
|
445
|
+
? `${field.label}. The correct default is an empty string — leave it blank to always show this element. Only provide a JavaScript expression if the task description explicitly states that this element should be conditionally hidden based on a URL state variable or the current user. Never invent field names or copy examples.`
|
|
446
|
+
: `${field.label}.${
|
|
447
|
+
field.sublabel ? ` ${field.sublabel}` : ""
|
|
448
|
+
}`),
|
|
407
449
|
...fieldProperties(field),
|
|
408
450
|
};
|
|
409
451
|
if (!properties[field.name].type) {
|
|
@@ -44,7 +44,7 @@ const requirementsList = async (req) => {
|
|
|
44
44
|
type: "CopilotConstructMgr",
|
|
45
45
|
name: "requirement",
|
|
46
46
|
},
|
|
47
|
-
{ orderBy: "written_at" }
|
|
47
|
+
{ orderBy: "written_at" }
|
|
48
48
|
);
|
|
49
49
|
const starFieldview = getState().types.Integer.fieldviews.show_star_rating;
|
|
50
50
|
|
|
@@ -67,43 +67,85 @@ const requirementsList = async (req) => {
|
|
|
67
67
|
class: "btn btn-outline-danger btn-sm",
|
|
68
68
|
onclick: `view_post("${viewname}", "del_req", {id:${r.id}})`,
|
|
69
69
|
},
|
|
70
|
-
i({ class: "fas fa-trash-alt" })
|
|
70
|
+
i({ class: "fas fa-trash-alt" })
|
|
71
71
|
),
|
|
72
72
|
},
|
|
73
73
|
],
|
|
74
|
-
rs
|
|
74
|
+
rs
|
|
75
75
|
),
|
|
76
76
|
button(
|
|
77
77
|
{
|
|
78
78
|
class: "btn btn-outline-danger mb-4",
|
|
79
79
|
onclick: `view_post("${viewname}", "del_all_reqs")`,
|
|
80
80
|
},
|
|
81
|
-
"Delete all"
|
|
82
|
-
)
|
|
81
|
+
"Delete all"
|
|
82
|
+
)
|
|
83
83
|
);
|
|
84
|
-
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const generating = await MetaData.findOne({
|
|
87
|
+
type: "CopilotConstructMgr",
|
|
88
|
+
name: "generating_requirements",
|
|
89
|
+
});
|
|
90
|
+
if (generating) {
|
|
85
91
|
return div(
|
|
86
92
|
{ class: "mt-2" },
|
|
87
|
-
p(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class: "btn btn-primary",
|
|
91
|
-
onclick: `press_store_button(this);view_post("${viewname}", "gen_reqs")`,
|
|
92
|
-
},
|
|
93
|
-
"Generate requirements",
|
|
93
|
+
p(
|
|
94
|
+
i({ class: "fas fa-spinner fa-spin me-2" }),
|
|
95
|
+
"Generating requirements, please wait..."
|
|
94
96
|
),
|
|
97
|
+
script(
|
|
98
|
+
domReady(`
|
|
99
|
+
(function() {
|
|
100
|
+
const poll = () => {
|
|
101
|
+
view_post(${JSON.stringify(viewname)}, 'req_status', {}, (resp) => {
|
|
102
|
+
if (resp && !resp.generating) location.reload();
|
|
103
|
+
else setTimeout(poll, 3000);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
setTimeout(poll, 3000);
|
|
107
|
+
})();
|
|
108
|
+
`)
|
|
109
|
+
)
|
|
95
110
|
);
|
|
96
111
|
}
|
|
112
|
+
|
|
113
|
+
return div(
|
|
114
|
+
{ class: "mt-2", id: "req-gen-area" },
|
|
115
|
+
p("No requirements found"),
|
|
116
|
+
button(
|
|
117
|
+
{ class: "btn btn-primary", onclick: `copilotGenReqs()` },
|
|
118
|
+
"Generate requirements"
|
|
119
|
+
),
|
|
120
|
+
script(
|
|
121
|
+
domReady(`
|
|
122
|
+
window.copilotGenReqs = () => {
|
|
123
|
+
document.getElementById('req-gen-area').innerHTML =
|
|
124
|
+
'<p><i class="fas fa-spinner fa-spin me-2"></i>Generating requirements, please wait...</p>';
|
|
125
|
+
view_post(${JSON.stringify(viewname)}, 'gen_reqs', {}, () => {});
|
|
126
|
+
const poll = () => {
|
|
127
|
+
view_post(${JSON.stringify(viewname)}, 'req_status', {}, (resp) => {
|
|
128
|
+
if (resp && !resp.generating) location.reload();
|
|
129
|
+
else setTimeout(poll, 3000);
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
setTimeout(poll, 3000);
|
|
133
|
+
};
|
|
134
|
+
`)
|
|
135
|
+
)
|
|
136
|
+
);
|
|
97
137
|
};
|
|
98
138
|
|
|
99
|
-
const
|
|
100
|
-
const
|
|
139
|
+
const doGenReqs = async (spec, userId) => {
|
|
140
|
+
const generatingMd = await MetaData.create({
|
|
101
141
|
type: "CopilotConstructMgr",
|
|
102
|
-
name: "
|
|
142
|
+
name: "generating_requirements",
|
|
143
|
+
body: {},
|
|
144
|
+
user_id: userId,
|
|
103
145
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
try {
|
|
147
|
+
const answer = await getState().functions.llm_generate.run(
|
|
148
|
+
`Generate the requirements for this application:
|
|
107
149
|
|
|
108
150
|
Description: ${spec.body.description}
|
|
109
151
|
Audience: ${spec.body.audience}
|
|
@@ -113,24 +155,44 @@ Visual style: ${spec.body.visual_style}
|
|
|
113
155
|
|
|
114
156
|
Now use the make_requirements tool to list the requirements for this software application
|
|
115
157
|
`,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
158
|
+
{
|
|
159
|
+
tools: [requirements_tool],
|
|
160
|
+
...tool_choice("make_requirements"),
|
|
161
|
+
systemPrompt:
|
|
162
|
+
"You are a project manager. The user wants to build an application, and you must analyse their application description",
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
const tc = answer.getToolCalls()[0];
|
|
166
|
+
for (const reqm of tc.input.requirements)
|
|
167
|
+
await MetaData.create({
|
|
168
|
+
type: "CopilotConstructMgr",
|
|
169
|
+
name: "requirement",
|
|
170
|
+
body: reqm,
|
|
171
|
+
user_id: userId,
|
|
172
|
+
});
|
|
173
|
+
} finally {
|
|
174
|
+
await generatingMd.delete();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
123
177
|
|
|
124
|
-
|
|
178
|
+
const gen_reqs = async (table_id, viewname, config, body, { req, res }) => {
|
|
179
|
+
const spec = await MetaData.findOne({
|
|
180
|
+
type: "CopilotConstructMgr",
|
|
181
|
+
name: "spec",
|
|
182
|
+
});
|
|
183
|
+
if (!spec) throw new Error("Specification not found");
|
|
184
|
+
doGenReqs(spec, req.user?.id).catch((e) =>
|
|
185
|
+
console.error("gen_reqs error", e)
|
|
186
|
+
);
|
|
187
|
+
return { json: { success: true } };
|
|
188
|
+
};
|
|
125
189
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
return { json: { reload_page: true } };
|
|
190
|
+
const req_status = async (table_id, viewname, config, body, { req, res }) => {
|
|
191
|
+
const generating = await MetaData.findOne({
|
|
192
|
+
type: "CopilotConstructMgr",
|
|
193
|
+
name: "generating_requirements",
|
|
194
|
+
});
|
|
195
|
+
return { json: { generating: !!generating } };
|
|
134
196
|
};
|
|
135
197
|
|
|
136
198
|
const del_req = async (table_id, viewname, config, body, { req, res }) => {
|
|
@@ -151,6 +213,6 @@ const del_all_reqs = async (table_id, viewname, config, body, { req, res }) => {
|
|
|
151
213
|
return { json: { reload_page: true } };
|
|
152
214
|
};
|
|
153
215
|
|
|
154
|
-
const req_routes = { gen_reqs, del_req, del_all_reqs };
|
|
216
|
+
const req_routes = { gen_reqs, req_status, del_req, del_all_reqs };
|
|
155
217
|
|
|
156
218
|
module.exports = { requirementsList, req_routes };
|
|
@@ -43,6 +43,7 @@ const runTask = async (md_id, req) => {
|
|
|
43
43
|
{ skill_type: "Generate trigger", yoloMode: true },
|
|
44
44
|
{ skill_type: "Generate View", yoloMode: true },
|
|
45
45
|
{ skill_type: "Install Plugin", yoloMode: true },
|
|
46
|
+
{ skill_type: "Registry editor", yoloMode: true },
|
|
46
47
|
],
|
|
47
48
|
},
|
|
48
49
|
});
|
|
@@ -58,49 +59,68 @@ Important: The database schema is already fully implemented. Do NOT use generate
|
|
|
58
59
|
|
|
59
60
|
Important: Some fields are non-stored (virtual) calculated fields — they have no database column and are computed on-the-fly by Saltcorn. Never include such fields in modify_row, SQL UPDATE statements, or recalculate_stored_fields calls. Only fields that exist as actual database columns (regular fields and stored calculated fields) can be written. If a calculated field needs updating, it will refresh automatically when the fields it depends on change.
|
|
60
61
|
|
|
61
|
-
Important: The "users" table is built-in. Passwords are platform-managed — never add a password field to a view. Signup uses
|
|
62
|
+
Important: The "users" table is built-in. Passwords are platform-managed — never add a password field to a view. Signup uses the built-in page at /auth/signup, login at /auth/login. Do NOT create triggers for registration or email verification — the platform handles this natively.
|
|
63
|
+
|
|
64
|
+
Important: On landing pages, place Log in / Create account buttons in no more than two locations (e.g. navbar and one hero call-to-action). Do not repeat them in a third "Get started" section or anywhere else. For links that take an already-authenticated user to their dashboard, use href="/" — not /auth/login.
|
|
65
|
+
|
|
66
|
+
Important: Do not name any page or view "Admin dashboard" — that name is reserved by the Saltcorn platform. For pages intended for role 1 (admin), use a name like "App admin dashboard" or prefix it with the application name (e.g. "Law Firm admin dashboard").
|
|
67
|
+
|
|
68
|
+
Important: When creating a page or view, always set min_role based on the intended audience: 1 for admin-only, 40 for staff and above, 80 for logged-in users and above, 100 for public. Never default to public (100) unless the page or view is explicitly intended for unauthenticated users (e.g. a landing page). A dashboard or view for clients/users is role 80, a staff page or view is role 40, an admin page or view is role 1.
|
|
69
|
+
|
|
70
|
+
Important: Two-factor authentication (2FA/TOTP) is fully built into the platform. To configure it, call set_entity directly with entity_type "system-configuration-value" and entity_name "twofa_policy_by_role". The entity_definition must be the plain JSON object itself — for example: {"1": "Mandatory", "100": "Disabled"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity and merge rather than overwrite. Do NOT create a workflow or trigger to do this.
|
|
71
|
+
|
|
72
|
+
Important: To set a page as the home page for a role, call set_entity directly with entity_type "system-configuration-value" and entity_name "home_page_by_role". The value is a JSON object mapping role IDs to page names — Role IDs: public=100, user=80, staff=40, admin=1. The entity_definition must be the plain JSON object itself — for example: {"100": "landing", "80": "client_dashboard"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity so you can merge rather than overwrite. Do NOT create a workflow or trigger to do this — use set_entity directly.
|
|
73
|
+
|
|
74
|
+
Important: If the task description mentions adding a viewlink, linking rows to another view, or a button that opens another view from a list — that viewlink column MUST be present in the finished view. Do not skip it. Viewlinks require calling get_relation_paths first to obtain the relation string before generating the layout.
|
|
75
|
+
|
|
76
|
+
Important: Before creating or updating any view or page that embeds, links to, or opens another view (including viewlinks, action buttons, and ajax_modal calls), call list_entities (entity_type "view") to get all existing view names. Only reference views that appear in that list — never invent a name or assume a view exists. If a view is not in the list, omit the reference entirely. Do the same for pages: call list_entities (entity_type "page") before linking to any page by name.
|
|
62
77
|
|
|
63
78
|
Your task now is:
|
|
64
79
|
${md.body.description}`;
|
|
65
|
-
const safeReq = req?.__
|
|
66
|
-
? req
|
|
67
|
-
: { ...req, __: (s) => s, user: req?.user };
|
|
80
|
+
const safeReq = req?.__ ? req : { ...req, __: (s) => s, user: req?.user };
|
|
68
81
|
|
|
69
82
|
await md.update({ body: { ...md.body, status: "Running" } });
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
try {
|
|
84
|
+
const actionres = await agent_action.runWithoutRow({
|
|
85
|
+
row: { prompt },
|
|
86
|
+
req: safeReq,
|
|
87
|
+
user: safeReq.user,
|
|
88
|
+
});
|
|
89
|
+
const run_id = actionres.json.run_id;
|
|
90
|
+
const run = await WorkflowRun.findOne({ id: run_id });
|
|
91
|
+
await agent_action.runWithoutRow({
|
|
92
|
+
row: {
|
|
93
|
+
prompt:
|
|
94
|
+
"Write a description of what you did, for the purposes of a progress report. Write 1-4 sentences. Do not use any tools or write any code",
|
|
95
|
+
},
|
|
96
|
+
req: safeReq,
|
|
97
|
+
run,
|
|
98
|
+
user: safeReq.user,
|
|
99
|
+
});
|
|
100
|
+
const updatedRun = await WorkflowRun.findOne({ id: run_id });
|
|
101
|
+
const lastInteraction =
|
|
102
|
+
updatedRun.context.interactions[
|
|
103
|
+
updatedRun.context.interactions.length - 1
|
|
104
|
+
];
|
|
105
|
+
const lastText =
|
|
106
|
+
typeof lastInteraction.content === "string"
|
|
107
|
+
? lastInteraction.content
|
|
91
108
|
: lastInteraction.content.text
|
|
92
109
|
? lastInteraction.content.text
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
: Array.isArray(lastInteraction.content)
|
|
111
|
+
? lastInteraction.content[0].text
|
|
112
|
+
: lastInteraction.content;
|
|
113
|
+
await MetaData.create({
|
|
114
|
+
type: "CopilotConstructMgr",
|
|
115
|
+
name: "progress",
|
|
116
|
+
body: { text: lastText, run_id, task_id: md.id },
|
|
117
|
+
user_id: req?.user?.id,
|
|
118
|
+
});
|
|
119
|
+
await md.update({ body: { ...md.body, status: "Done", run_id } });
|
|
120
|
+
} catch (e) {
|
|
121
|
+
await md.update({ body: { ...md.body, status: "To do" } });
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
104
124
|
};
|
|
105
125
|
|
|
106
126
|
/**
|
|
@@ -128,9 +148,12 @@ const runNextTask = async (once = false) => {
|
|
|
128
148
|
);
|
|
129
149
|
const done = tasks.filter((t) => t.body.status === "Done");
|
|
130
150
|
const done_names = new Set(done.map((t) => t.body.name));
|
|
151
|
+
const all_task_names = new Set(tasks.map((t) => t.body.name).filter(Boolean));
|
|
131
152
|
|
|
132
153
|
const startable = todos.filter((t) =>
|
|
133
|
-
t.body.depends_on
|
|
154
|
+
(t.body.depends_on || []).every(
|
|
155
|
+
(nm) => done_names.has(nm) || !all_task_names.has(nm)
|
|
156
|
+
)
|
|
134
157
|
);
|
|
135
158
|
|
|
136
159
|
if (startable[0]) {
|
|
@@ -140,6 +163,13 @@ const runNextTask = async (once = false) => {
|
|
|
140
163
|
: null;
|
|
141
164
|
await runTask(startable[0].id, { user: taskUser, __: (s) => s });
|
|
142
165
|
if (!once) await runNextTask();
|
|
166
|
+
} else if (!once) {
|
|
167
|
+
const settings = await MetaData.findOne({
|
|
168
|
+
type: "CopilotConstructMgr",
|
|
169
|
+
name: "settings",
|
|
170
|
+
});
|
|
171
|
+
if (settings?.body?.running)
|
|
172
|
+
await settings.update({ body: { ...settings.body, running: false } });
|
|
143
173
|
}
|
|
144
174
|
};
|
|
145
175
|
|