@saltcorn/copilot 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  const Table = require("@saltcorn/data/models/table");
2
2
  const View = require("@saltcorn/data/models/view");
3
3
  const { fieldProperties } = require("../common");
4
+ const { initial_config_all_fields } = require("@saltcorn/data/plugin-helper");
4
5
  const { getState } = require("@saltcorn/data/db/state");
5
6
  const {
6
7
  div,
@@ -12,6 +13,91 @@ const {
12
13
  iframe,
13
14
  text_attr,
14
15
  } = require("@saltcorn/markup/tags");
16
+ const builderGen = require("../builder-gen");
17
+
18
+ const collectLayoutFieldNames = (segment, out = new Set()) => {
19
+ if (!segment || typeof segment !== "object") return out;
20
+ if (Array.isArray(segment)) {
21
+ segment.forEach((s) => collectLayoutFieldNames(s, out));
22
+ return out;
23
+ }
24
+ if (segment.type === "field" && segment.field_name)
25
+ out.add(segment.field_name);
26
+ if (segment.above) collectLayoutFieldNames(segment.above, out);
27
+ if (segment.besides) collectLayoutFieldNames(segment.besides, out);
28
+ if (segment.contents) collectLayoutFieldNames(segment.contents, out);
29
+ if (Array.isArray(segment.tabs))
30
+ segment.tabs.forEach((t) => collectLayoutFieldNames(t?.contents, out));
31
+ return out;
32
+ };
33
+
34
+ const findFilterFieldSegment = (segment) => {
35
+ if (!segment || typeof segment !== "object") return null;
36
+ if (segment.type === "field") return segment;
37
+ if (segment.type === "dropdown_filter" || segment.type === "toggle_filter") {
38
+ return { field_name: segment.field_name, fieldview: "edit" };
39
+ }
40
+ if (Array.isArray(segment.above)) {
41
+ for (const item of segment.above) {
42
+ const found = findFilterFieldSegment(item);
43
+ if (found) return found;
44
+ }
45
+ }
46
+ if (Array.isArray(segment.besides)) {
47
+ for (const item of segment.besides) {
48
+ const found = findFilterFieldSegment(item);
49
+ if (found) return found;
50
+ }
51
+ }
52
+ if (segment.contents) {
53
+ if (Array.isArray(segment.contents)) {
54
+ for (const item of segment.contents) {
55
+ const found = findFilterFieldSegment(item);
56
+ if (found) return found;
57
+ }
58
+ } else {
59
+ const found = findFilterFieldSegment(segment.contents);
60
+ if (found) return found;
61
+ }
62
+ }
63
+ if (Array.isArray(segment.tabs)) {
64
+ for (const tab of segment.tabs) {
65
+ if (tab?.contents) {
66
+ const found = findFilterFieldSegment(tab.contents);
67
+ if (found) return found;
68
+ }
69
+ }
70
+ }
71
+ if (Array.isArray(segment.contents) && Array.isArray(segment.contents[0])) {
72
+ for (const row of segment.contents) {
73
+ if (Array.isArray(row)) {
74
+ for (const cell of row) {
75
+ const found = findFilterFieldSegment(cell);
76
+ if (found) return found;
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return null;
82
+ };
83
+
84
+ const normalizeFilterField = (segment) => ({
85
+ type: "field",
86
+ field_name: segment.field_name,
87
+ fieldview: segment.fieldview || "edit",
88
+ textStyle: segment.textStyle || "",
89
+ block: segment.block ?? false,
90
+ configuration: segment.configuration || {},
91
+ });
92
+
93
+ const toFilterColumn = (segment) => ({
94
+ type: "Field",
95
+ field_name: segment.field_name,
96
+ fieldview: segment.fieldview || "edit",
97
+ textStyle: segment.textStyle || "",
98
+ block: segment.block ?? false,
99
+ configuration: segment.configuration || {},
100
+ });
15
101
 
16
102
  class GenerateViewSkill {
17
103
  static skill_name = "Generate View";
@@ -25,8 +111,11 @@ class GenerateViewSkill {
25
111
  }
26
112
 
27
113
  async systemPrompt() {
28
- return `If the user asks to generate a view, use the generate_view tool to enter
29
- a view generation mode. The tool call only requires high-level details to start this sequence.`;
114
+ return (
115
+ `If the user asks to generate a view, use the generate_view tool to enter ` +
116
+ `a view generation mode. The tool call only requires high-level details to start this sequence.\n` +
117
+ `The Edit viewtemplate serves both create (no id in state) and edit (id in state) — one view covers both modes.`
118
+ );
30
119
  }
31
120
 
32
121
  get userActions() {
@@ -38,13 +127,20 @@ a view generation mode. The tool call only requires high-level details to start
38
127
  table,
39
128
  min_role,
40
129
  }) {
130
+ const normalizedRole = min_role || "public";
131
+ const tableRow = table ? Table.findOne({ name: table }) : null;
41
132
  await View.create({
42
133
  name,
43
134
  viewtemplate: viewpattern,
44
- table: Table.findOne({ name: table }),
45
- min_role: { admin: 1, public: 100, user: 80 }[min_role],
135
+ table_id: tableRow?.id,
136
+ table: tableRow,
137
+ min_role: { admin: 1, public: 100, user: 80 }[normalizedRole],
46
138
  configuration: wfctx,
47
139
  });
140
+ const vt = getState().viewtemplates[viewpattern];
141
+ if (vt?.copilot_post_create) {
142
+ await vt.copilot_post_create({ name, configuration: wfctx });
143
+ }
48
144
  setTimeout(() => getState().refresh_views(), 200);
49
145
  return {
50
146
  notify: `View saved: <a target="_blank" href="/view/${name}">${name}</a>`,
@@ -61,12 +157,17 @@ a view generation mode. The tool call only requires high-level details to start
61
157
  const enabled_vt_names = all_vt_names.filter(
62
158
  (vtnm) =>
63
159
  vts[vtnm].enable_copilot_viewgen ||
64
- vts[vtnm].copilot_generate_view_prompt,
160
+ vts[vtnm].copilot_generate_view_prompt
65
161
  );
162
+ if (!enabled_vt_names.includes("Show")) enabled_vt_names.push("Show");
163
+ if (!enabled_vt_names.includes("Edit")) enabled_vt_names.push("Edit");
164
+ if (!enabled_vt_names.includes("List")) enabled_vt_names.push("List");
165
+ if (!enabled_vt_names.includes("Filter")) enabled_vt_names.push("Filter");
66
166
  //const roles = await User.get_roles();
67
167
  const tableless = enabled_vt_names.filter(
68
- (vtnm) => vts[vtnm].tableless === true,
168
+ (vtnm) => vts[vtnm].tableless === true
69
169
  );
170
+ const roles = state.roles;
70
171
  const parameters = {
71
172
  type: "object",
72
173
  required: ["name", "viewpattern"],
@@ -76,7 +177,9 @@ a view generation mode. The tool call only requires high-level details to start
76
177
  type: "string",
77
178
  },
78
179
  viewpattern: {
79
- description: `The type of view to generate. Some of the view descriptions: ${enabled_vt_names.map((vtnm) => `${vtnm}: ${vts[vtnm].description}.`).join(" ")}`,
180
+ description: `The type of view to generate. Some of the view descriptions: ${enabled_vt_names
181
+ .map((vtnm) => `${vtnm}: ${vts[vtnm].description}.`)
182
+ .join(" ")}`,
80
183
  type: "string",
81
184
  enum: enabled_vt_names,
82
185
  },
@@ -91,7 +194,7 @@ a view generation mode. The tool call only requires high-level details to start
91
194
  description:
92
195
  "The minimum role needed to access the view. For views accessible only by admin, use 'admin', pages with min_role 'public' is publicly accessible and also available to all users",
93
196
  type: "string",
94
- enum: ["admin", "user", "public"],
197
+ enum: roles ? roles.map((r) => r.role) : ["admin", "user", "public"],
95
198
  },
96
199
  },
97
200
  };
@@ -107,73 +210,219 @@ a view generation mode. The tool call only requires high-level details to start
107
210
  process: async (input) => {
108
211
  return "Metadata received";
109
212
  },
110
- postProcess: async ({ tool_call, req, generate }) => {
213
+ postProcess: async ({ tool_call, req, generate, chat }) => {
111
214
  const state = getState();
112
215
  const vt = state.viewtemplates[tool_call.input.viewpattern];
113
216
  const table =
114
217
  vt.tableless === true
115
218
  ? null
116
219
  : Table.findOne({ name: tool_call.input.table });
117
- const flow = vt.configuration_workflow(req);
220
+
118
221
  const wfctx = { viewname: tool_call.input.name, table_id: table?.id };
119
- let vt_prompt = "";
120
- if (vt.copilot_generate_view_prompt) {
121
- if (typeof vt.copilot_generate_view_prompt === "string")
122
- vt_prompt = vt.copilot_generate_view_prompt;
123
- else if (typeof vt.copilot_generate_view_prompt === "function")
124
- vt_prompt = await vt.copilot_generate_view_prompt(tool_call.input);
222
+ const viewpattern = tool_call.input.viewpattern;
223
+ const builderModeByPattern = {
224
+ Show: "show",
225
+ Edit: "edit",
226
+ List: "listcolumns",
227
+ Filter: "filter",
228
+ };
229
+ const builderMode = builderModeByPattern[viewpattern];
230
+ if (builderMode) {
231
+ const extractText = (c) => {
232
+ if (typeof c === "string") return c;
233
+ if (Array.isArray(c)) {
234
+ const textPart = c.find(
235
+ (p) => p?.type === "text" || typeof p === "string"
236
+ );
237
+ return (
238
+ textPart?.text || (typeof textPart === "string" ? textPart : "")
239
+ );
240
+ }
241
+ return "";
242
+ };
243
+ const isToolResultMessage = (item) => {
244
+ if (!Array.isArray(item?.content)) return false;
245
+ return item.content.every((p) => p?.type === "tool_result");
246
+ };
247
+ const promptFromChat = Array.isArray(chat)
248
+ ? (() => {
249
+ const userMsgs = chat.filter(
250
+ (item) =>
251
+ item?.role === "user" &&
252
+ item?.content &&
253
+ !isToolResultMessage(item)
254
+ );
255
+ return userMsgs.length ? extractText(userMsgs[0].content) : "";
256
+ })()
257
+ : "";
258
+ const layoutPrompt = promptFromChat || tool_call.input.name || "";
259
+ wfctx.layout = await builderGen.run(
260
+ layoutPrompt,
261
+ builderMode,
262
+ table?.name,
263
+ null,
264
+ chat
265
+ );
266
+ if (table && viewpattern !== "Filter") {
267
+ const baseCfg = await initial_config_all_fields(false)({
268
+ table_id: table.id,
269
+ });
270
+ if (baseCfg?.columns) wfctx.columns = baseCfg.columns;
271
+ }
272
+ if (viewpattern === "Edit" && table) {
273
+ const layoutFieldNames = collectLayoutFieldNames(wfctx.layout);
274
+ const fields = table.fields || [];
275
+ const fixed = {};
276
+ for (const f of fields) {
277
+ if (f.primary_key || f.calculated) continue;
278
+ if (layoutFieldNames.has(f.name)) continue;
279
+ if (f.type === "Key" && f.reftable_name === "users") {
280
+ fixed[`preset_${f.name}`] = "LoggedIn";
281
+ fixed[`_block_${f.name}`] = true;
282
+ }
283
+ }
284
+ if (Object.keys(fixed).length > 0) wfctx.fixed = fixed;
285
+ wfctx.destination_type = "Back to referer";
286
+ }
287
+ if (viewpattern === "Filter") {
288
+ const filterFieldSegment = findFilterFieldSegment(wfctx.layout);
289
+ if (filterFieldSegment) {
290
+ const normalized = normalizeFilterField(filterFieldSegment);
291
+ wfctx.layout = normalized;
292
+ wfctx.columns = [toFilterColumn(normalized)];
293
+ }
294
+ }
125
295
  }
126
296
 
127
- for (const step of flow.steps) {
128
- const form = await step.form(wfctx);
129
- const properties = {};
130
- //TODO onlyWhen
131
- for (const field of form.fields) {
132
- //TODO showIf
133
- properties[field.name] = {
134
- description:
135
- field.copilot_description ||
136
- `${field.label}.${field.sublabel ? ` ${field.sublabel}` : ""}`,
137
- ...fieldProperties(field),
138
- };
297
+ if (
298
+ viewpattern === "Show" ||
299
+ viewpattern === "Edit" ||
300
+ viewpattern === "Filter"
301
+ ) {
302
+ // No extra configuration steps for these modes.
303
+ } else {
304
+ const flow = vt.configuration_workflow(req);
305
+ let vt_prompt = "";
306
+ if (vt.copilot_generate_view_prompt) {
307
+ if (typeof vt.copilot_generate_view_prompt === "string")
308
+ vt_prompt = vt.copilot_generate_view_prompt;
309
+ else if (typeof vt.copilot_generate_view_prompt === "function")
310
+ vt_prompt = await vt.copilot_generate_view_prompt(
311
+ tool_call.input
312
+ );
139
313
  }
140
314
 
141
- const answer = await generate(
142
- `${vt_prompt ? vt_prompt + "\n\n" : ""}Now generate the ${step.name} details of the view by calling the generate_view_details tool`,
143
- {
144
- tools: [
145
- {
315
+ const prefilledFields = new Set();
316
+ if (wfctx.layout !== undefined) prefilledFields.add("layout");
317
+ if (wfctx.columns !== undefined) prefilledFields.add("columns");
318
+
319
+ // For List views: pre-fill view_to_create with the best Edit view for the table
320
+ if (viewpattern === "List" && table) {
321
+ const candidateViews = await View.find_table_views_where(
322
+ table.id,
323
+ ({ state_fields, viewrow }) =>
324
+ viewrow.name !== tool_call.input.name &&
325
+ state_fields.every((sf) => !sf.required)
326
+ );
327
+ if (candidateViews.length > 0) {
328
+ const editView =
329
+ candidateViews.find((v) =>
330
+ v.name.toLowerCase().includes("edit")
331
+ ) || candidateViews[0];
332
+ wfctx.view_to_create =
333
+ editView.select_option?.name || editView.name;
334
+ wfctx.create_view_display = "Popup";
335
+ wfctx.create_view_location = "Top right";
336
+ prefilledFields.add("view_to_create");
337
+ prefilledFields.add("create_view_display");
338
+ prefilledFields.add("create_view_location");
339
+ }
340
+ }
341
+
342
+ for (const step of flow.steps) {
343
+ if (typeof step.form !== "function") continue;
344
+ const form = await step.form(wfctx);
345
+ const properties = {};
346
+ //TODO onlyWhen
347
+ for (const field of form.fields) {
348
+ if (prefilledFields.has(field.name)) continue;
349
+ //TODO showIf
350
+ properties[field.name] = {
351
+ description:
352
+ field.copilot_description ||
353
+ `${field.label}.${
354
+ field.sublabel ? ` ${field.sublabel}` : ""
355
+ }`,
356
+ ...fieldProperties(field),
357
+ };
358
+ if (!properties[field.name].type) {
359
+ properties[field.name].type = "string";
360
+ }
361
+ }
362
+
363
+ if (!Object.keys(properties).length) continue;
364
+
365
+ const answer = await generate(
366
+ `${vt_prompt ? vt_prompt + "\n\n" : ""}Now generate the ${
367
+ step.name
368
+ } details of the view by calling the generate_view_details tool`,
369
+ {
370
+ tools: [
371
+ {
372
+ type: "function",
373
+ function: {
374
+ name: "generate_view_details",
375
+ description: "Provide view details",
376
+ parameters: {
377
+ type: "object",
378
+ properties,
379
+ },
380
+ },
381
+ },
382
+ ],
383
+ tool_choice: {
146
384
  type: "function",
147
385
  function: {
148
386
  name: "generate_view_details",
149
- description: "Provide view details",
150
- parameters: {
151
- type: "object",
152
- properties,
153
- },
154
387
  },
155
388
  },
156
- ],
157
- tool_choice: {
158
- type: "function",
159
- function: {
160
- name: "generate_view_details",
161
- },
162
- },
163
- },
164
- );
165
- const tc = answer.getToolCalls()[0];
166
- Object.assign(wfctx, tc.input);
389
+ }
390
+ );
391
+ const tc = answer.getToolCalls()[0];
392
+ await getState().functions.llm_add_message.run(
393
+ "tool_response",
394
+ { type: "text", value: "Details provided" },
395
+ { chat, tool_call: tc }
396
+ );
397
+ Object.assign(wfctx, tc.input);
398
+ }
167
399
  }
400
+ const roleName = tool_call.input.min_role || "public";
401
+ const rolesState = getState().roles;
402
+ const min_role = rolesState
403
+ ? (rolesState.find((r) => r.role === roleName) || { id: 100 }).id
404
+ : { admin: 1, public: 100, user: 80 }[roleName] ?? 100;
168
405
  const view = new View({
169
406
  name: tool_call.input.name,
170
407
  viewtemplate: tool_call.input.viewpattern,
171
408
  table,
172
- min_role: { admin: 1, public: 100, user: 80 }[
173
- tool_call.input.min_role
174
- ],
409
+ table_id: table?.id,
410
+ min_role,
175
411
  configuration: wfctx,
176
412
  });
413
+ if (this.yoloMode) {
414
+ await this.userActions.build_copilot_view_gen({
415
+ wfctx,
416
+ name: tool_call.input.name,
417
+ viewpattern: tool_call.input.viewpattern,
418
+ table: tool_call.input.table,
419
+ min_role: tool_call.input.min_role,
420
+ });
421
+ return {
422
+ stop: true,
423
+ add_response: `View ${tool_call.input.name} created.`,
424
+ };
425
+ }
177
426
  const runres = await view.run({}, { req });
178
427
  return {
179
428
  stop: true,
@@ -181,7 +430,7 @@ a view generation mode. The tool call only requires high-level details to start
181
430
  pre(JSON.stringify(wfctx, null, 2)) +
182
431
  div(
183
432
  { style: { maxHeight: 800, maxWidth: 500, overflow: "scroll" } },
184
- runres,
433
+ runres
185
434
  ),
186
435
  add_user_action: {
187
436
  name: "build_copilot_view_gen",
@@ -550,11 +550,50 @@ class GenerateWorkflowSkill {
550
550
  ensureActionCatalog();
551
551
  }
552
552
 
553
+ static async configFields() {
554
+ return [
555
+ {
556
+ name: "context_vars",
557
+ label: "Initial context variables",
558
+ input_type: "code",
559
+ attributes: { mode: "application/json" },
560
+ sublabel:
561
+ 'JSON object of key-value pairs pre-loaded into the workflow context at startup (e.g. {"ELEVENLABS_API_KEY": "sk-..."}). Keys are available by name in every run_js_code step.',
562
+ },
563
+ ];
564
+ }
565
+
566
+ _parseContextVars() {
567
+ if (!this.context_vars) return null;
568
+ try {
569
+ const vars =
570
+ typeof this.context_vars === "string"
571
+ ? JSON.parse(this.context_vars)
572
+ : this.context_vars;
573
+ return Object.keys(vars).length ? vars : null;
574
+ } catch {
575
+ return null;
576
+ }
577
+ }
578
+
553
579
  async systemPrompt() {
554
- return await GenerateWorkflow.system_prompt();
580
+ const base = await GenerateWorkflow.system_prompt();
581
+ const vars = this._parseContextVars();
582
+ if (!vars) return base;
583
+ const keyList = Object.keys(vars).join(", ");
584
+ return (
585
+ base +
586
+ `\n\nThe following values are pre-loaded into the workflow context before the first step runs: ${keyList}. ` +
587
+ `Use them directly by name in run_js_code steps (e.g. \`${
588
+ Object.keys(vars)[0]
589
+ }\`) ` +
590
+ `or via the context object (e.g. \`context.${Object.keys(vars)[0]}\`). ` +
591
+ `Do not ask the user to supply these values — they are already available.`
592
+ );
555
593
  }
556
594
 
557
595
  get userActions() {
596
+ const context_vars = this._parseContextVars();
558
597
  return {
559
598
  async apply_copilot_workflow({ user, ...raw }) {
560
599
  const payload = ensureWorkflowHasSteps(normalizeWorkflowPayload(raw));
@@ -563,7 +602,11 @@ class GenerateWorkflowSkill {
563
602
  return {
564
603
  notify: `Cannot create workflow: ${analysis.blocking.join("; ")}`,
565
604
  };
566
- const result = await GenerateWorkflow.execute(payload, { user });
605
+ const result = await GenerateWorkflow.execute(
606
+ payload,
607
+ { user },
608
+ context_vars
609
+ );
567
610
  return {
568
611
  notify:
569
612
  result?.postExec ||
@@ -608,6 +651,13 @@ class GenerateWorkflowSkill {
608
651
  const canCreate =
609
652
  analysis.blocking.length === 0 &&
610
653
  preparedPayload.workflow_steps.length > 0;
654
+ if (this.yoloMode && canCreate) {
655
+ await this.userActions.apply_copilot_workflow(preparedPayload);
656
+ return {
657
+ stop: true,
658
+ add_response: `Workflow ${preparedPayload.workflow_name || "(unnamed)"} created.`,
659
+ };
660
+ }
611
661
  return {
612
662
  stop: true,
613
663
  add_response: `${issuesHtml}${previewHtml}`,
@@ -0,0 +1,12 @@
1
+ const viewname = "Saltcorn AppConstructor (experimental)";
2
+
3
+ const tool_choice = (tool_name) => ({
4
+ tool_choice: {
5
+ type: "function",
6
+ function: {
7
+ name: tool_name,
8
+ },
9
+ },
10
+ });
11
+
12
+ module.exports = { viewname, tool_choice };
@@ -0,0 +1,102 @@
1
+ const Field = require("@saltcorn/data/models/field");
2
+ const Table = require("@saltcorn/data/models/table");
3
+ const Form = require("@saltcorn/data/models/form");
4
+ const MetaData = require("@saltcorn/data/models/metadata");
5
+ const View = require("@saltcorn/data/models/view");
6
+ const Trigger = require("@saltcorn/data/models/trigger");
7
+ const { findType } = require("@saltcorn/data/models/discovery");
8
+ const { save_menu_items } = require("@saltcorn/data/models/config");
9
+ const db = require("@saltcorn/data/db");
10
+ const WorkflowRun = require("@saltcorn/data/models/workflow_run");
11
+ const {
12
+ localeDateTime,
13
+ renderForm,
14
+ mkTable,
15
+ post_delete_btn,
16
+ } = require("@saltcorn/markup");
17
+ const {
18
+ div,
19
+ script,
20
+ domReady,
21
+ pre,
22
+ code,
23
+ input,
24
+ h4,
25
+ style,
26
+ h5,
27
+ button,
28
+ text_attr,
29
+ i,
30
+ p,
31
+ span,
32
+ small,
33
+ form,
34
+ textarea,
35
+ } = require("@saltcorn/markup/tags");
36
+ const { getState } = require("@saltcorn/data/db/state");
37
+ const renderLayout = require("@saltcorn/markup/layout");
38
+ const { viewname } = require("./common");
39
+
40
+ const errorList = async (req) => {
41
+ const errs = await MetaData.find({
42
+ type: "CopilotConstructMgr",
43
+ name: "error",
44
+ });
45
+ if (errs.length) {
46
+ return div(
47
+ { class: "mt-2" },
48
+ mkTable(
49
+ [
50
+ { label: "Status", key: (m) => m.body.status },
51
+ {
52
+ label: "Error",
53
+ key: (m) => pre(JSON.stringify(m.body.error, null, 2)),
54
+ },
55
+ {
56
+ label: "Delete",
57
+ key: (r) =>
58
+ button(
59
+ {
60
+ class: "btn btn-outline-danger btn-sm",
61
+ onclick: `view_post("${viewname}", "del_err", {id:${r.id}})`,
62
+ },
63
+ i({ class: "fas fa-trash-alt" }),
64
+ ),
65
+ },
66
+ ],
67
+ errs,
68
+ ),
69
+ button(
70
+ {
71
+ class: "btn btn-outline-danger",
72
+ onclick: `view_post("${viewname}", "del_all_errs")`,
73
+ },
74
+ "Delete all",
75
+ ),
76
+ );
77
+ } else {
78
+ return div({ class: "mt-2" }, p("No errors"));
79
+ }
80
+ };
81
+
82
+ const del_err = async (table_id, viewname, config, body, { req, res }) => {
83
+ const r = await MetaData.findOne({
84
+ id: body.id,
85
+ });
86
+
87
+ if (!r) throw new Error("Error not found");
88
+ await r.delete();
89
+ return { json: { reload_page: true } };
90
+ };
91
+ const del_all_errs = async (table_id, viewname, config, body, { req, res }) => {
92
+ const rs = await MetaData.find({
93
+ type: "CopilotConstructMgr",
94
+ name: "error",
95
+ });
96
+ for (const r of rs) await r.delete();
97
+ return { json: { reload_page: true } };
98
+ };
99
+
100
+ const error_routes = { del_err, del_all_errs };
101
+
102
+ module.exports = { errorList, error_routes };