@saltcorn/copilot 0.8.0 → 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.
@@ -1,7 +1,10 @@
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
+ const {
5
+ initial_config_all_fields,
6
+ build_schema_data,
7
+ } = require("@saltcorn/data/plugin-helper");
5
8
  const { getState } = require("@saltcorn/data/db/state");
6
9
  const {
7
10
  div,
@@ -14,6 +17,26 @@ const {
14
17
  text_attr,
15
18
  } = require("@saltcorn/markup/tags");
16
19
  const builderGen = require("../builder-gen");
20
+ const {
21
+ RELATION_PATH_DOC,
22
+ GET_RELATION_PATHS_FUNCTION,
23
+ getRelationPathsForPairs,
24
+ } = require("../relation-paths");
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
+ };
17
40
 
18
41
  const collectLayoutFieldNames = (segment, out = new Set()) => {
19
42
  if (!segment || typeof segment !== "object") return out;
@@ -112,14 +135,35 @@ class GenerateViewSkill {
112
135
 
113
136
  async systemPrompt() {
114
137
  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.`
138
+ `If the user asks to generate a view, use the generate_view tool but ONLY if the view does not already exist. ` +
139
+ `If a view with that name already exists, do NOT call generate_view doing so will create a duplicate. Instead follow the modification sequence below.\n` +
140
+ `The Edit viewtemplate serves both create (no id in state) and edit (id in state) — one view covers both.\n\n` +
141
+ `**Modifying an existing view — required sequence:**\n` +
142
+ `(1) Call get_view_config to fetch the current configuration.\n` +
143
+ `(2) Only if you are adding view_link columns or embedded view (type "view") segments: call get_relation_paths once with all the source_table/target_view pairs you need. For changes that don't involve linking or embedding views (e.g. adding a field, changing a label), skip this step.\n` +
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` +
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` +
146
+ `**Generating a new view that contains view_links or embedded views:**\n` +
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` +
149
+ `**Embedded view segment format (for Show layouts):**\n` +
150
+ ` { "type": "view", "view": "<viewName>", "name": "<viewName>", "relation": "<from get_relation_paths>" }\n` +
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` +
152
+ RELATION_PATH_DOC
118
153
  );
119
154
  }
120
155
 
121
156
  get userActions() {
122
157
  return {
158
+ async build_copilot_view_update({ name, configuration }) {
159
+ const existingView = View.findOne({ name });
160
+ if (!existingView) return { error: `View "${name}" not found` };
161
+ await View.update({ configuration }, existingView.id);
162
+ setTimeout(() => getState().refresh_views(), 200);
163
+ return {
164
+ notify: `View updated: <a target="_blank" href="/view/${name}">${name}</a>`,
165
+ };
166
+ },
123
167
  async build_copilot_view_gen({
124
168
  wfctx,
125
169
  name,
@@ -127,14 +171,28 @@ class GenerateViewSkill {
127
171
  table,
128
172
  min_role,
129
173
  }) {
130
- const normalizedRole = min_role || "public";
174
+ const existing = View.findOne({ name });
175
+ if (existing)
176
+ return {
177
+ error: `View "${name}" already exists. Use get_view_config and apply_view_config to update it.`,
178
+ };
131
179
  const tableRow = table ? Table.findOne({ name: table }) : null;
180
+ const roleName =
181
+ typeof min_role === "number" ? null : min_role || "public";
182
+ const resolvedRole =
183
+ typeof min_role === "number"
184
+ ? min_role
185
+ : (
186
+ (getState().roles || []).find((r) => r.role === roleName) || {
187
+ id: 100,
188
+ }
189
+ ).id;
132
190
  await View.create({
133
191
  name,
134
192
  viewtemplate: viewpattern,
135
193
  table_id: tableRow?.id,
136
194
  table: tableRow,
137
- min_role: { admin: 1, public: 100, user: 80 }[normalizedRole],
195
+ min_role: resolvedRole,
138
196
  configuration: wfctx,
139
197
  });
140
198
  const vt = getState().viewtemplates[viewpattern];
@@ -199,12 +257,12 @@ class GenerateViewSkill {
199
257
  },
200
258
  };
201
259
 
202
- return {
260
+ const generateViewTool = {
203
261
  type: "function",
204
262
  function: {
205
263
  name: "generate_view",
206
264
  description:
207
- "Generate a view by supplying high-level details. This will trigger a view generation sequence",
265
+ "Generate a NEW view by supplying high-level details. Only call this for views that do not yet exist — if the view already exists, use get_view_config + apply_view_config instead.",
208
266
  parameters,
209
267
  },
210
268
  process: async (input) => {
@@ -264,23 +322,55 @@ class GenerateViewSkill {
264
322
  chat
265
323
  );
266
324
  if (table && viewpattern !== "Filter") {
267
- const baseCfg = await initial_config_all_fields(false)({
325
+ // isEdit=true: FK fields get Field+select columns; false gives JoinField (display-only)
326
+ const isEditView = viewpattern === "Edit";
327
+ const baseCfg = await initial_config_all_fields(isEditView)({
268
328
  table_id: table.id,
269
329
  });
270
330
  if (baseCfg?.columns) wfctx.columns = baseCfg.columns;
271
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
+ }
272
347
  if (viewpattern === "Edit" && table) {
273
348
  const layoutFieldNames = collectLayoutFieldNames(wfctx.layout);
274
349
  const fields = table.fields || [];
275
350
  const fixed = {};
351
+ const usersFkColumnsToAdd = [];
276
352
  for (const f of fields) {
277
353
  if (f.primary_key || f.calculated) continue;
278
- if (layoutFieldNames.has(f.name)) continue;
279
354
  if (f.type === "Key" && f.reftable_name === "users") {
280
- fixed[`preset_${f.name}`] = "LoggedIn";
281
- fixed[`_block_${f.name}`] = true;
355
+ if (layoutFieldNames.has(f.name)) {
356
+ // Explicitly placed in layout — add a select column so getForm renders it
357
+ usersFkColumnsToAdd.push({
358
+ field_name: f.name,
359
+ type: "Field",
360
+ fieldview: "select",
361
+ state_field: true,
362
+ });
363
+ } else {
364
+ fixed[`preset_${f.name}`] = "LoggedIn";
365
+ fixed[`_block_${f.name}`] = true;
366
+ }
282
367
  }
283
368
  }
369
+ if (usersFkColumnsToAdd.length > 0)
370
+ wfctx.columns = [
371
+ ...(wfctx.columns || []),
372
+ ...usersFkColumnsToAdd,
373
+ ];
284
374
  if (Object.keys(fixed).length > 0) wfctx.fixed = fixed;
285
375
  wfctx.destination_type = "Back to referer";
286
376
  }
@@ -347,12 +437,15 @@ class GenerateViewSkill {
347
437
  for (const field of form.fields) {
348
438
  if (prefilledFields.has(field.name)) continue;
349
439
  //TODO showIf
440
+ const isShowif = field.name.endsWith("_showif");
350
441
  properties[field.name] = {
351
442
  description:
352
443
  field.copilot_description ||
353
- `${field.label}.${
354
- field.sublabel ? ` ${field.sublabel}` : ""
355
- }`,
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
+ }`),
356
449
  ...fieldProperties(field),
357
450
  };
358
451
  if (!properties[field.name].type) {
@@ -388,13 +481,18 @@ class GenerateViewSkill {
388
481
  },
389
482
  }
390
483
  );
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);
484
+ const tc =
485
+ typeof answer?.getToolCalls === "function"
486
+ ? answer.getToolCalls()[0]
487
+ : null;
488
+ if (tc) {
489
+ await getState().functions.llm_add_message.run(
490
+ "tool_response",
491
+ { type: "text", value: "Details provided" },
492
+ { chat, tool_call: tc }
493
+ );
494
+ Object.assign(wfctx, tc.input);
495
+ }
398
496
  }
399
497
  }
400
498
  const roleName = tool_call.input.min_role || "public";
@@ -402,6 +500,13 @@ class GenerateViewSkill {
402
500
  const min_role = rolesState
403
501
  ? (rolesState.find((r) => r.role === roleName) || { id: 100 }).id
404
502
  : { admin: 1, public: 100, user: 80 }[roleName] ?? 100;
503
+ const existingView = View.findOne({ name: tool_call.input.name });
504
+ if (existingView) {
505
+ return {
506
+ stop: true,
507
+ add_response: `Error: view "${tool_call.input.name}" already exists. Do NOT call generate_view again — use get_view_config to inspect the current configuration and apply_view_config to update it.`,
508
+ };
509
+ }
405
510
  const view = new View({
406
511
  name: tool_call.input.name,
407
512
  viewtemplate: tool_call.input.viewpattern,
@@ -441,6 +546,127 @@ class GenerateViewSkill {
441
546
  };
442
547
  },
443
548
  };
549
+
550
+ const getViewConfigTool = {
551
+ type: "function",
552
+ function: {
553
+ name: "get_view_config",
554
+ description:
555
+ "Retrieve the current configuration of an existing view. " +
556
+ "Call this first to inspect the layout before calling apply_view_config to save changes. " +
557
+ "Returns the full configuration JSON and the viewtemplate name.",
558
+ parameters: {
559
+ type: "object",
560
+ required: ["name"],
561
+ properties: {
562
+ name: {
563
+ description: "The name of the existing view to inspect.",
564
+ type: "string",
565
+ },
566
+ },
567
+ },
568
+ },
569
+ process: async ({ name }) => {
570
+ const existingView = View.findOne({ name });
571
+ if (!existingView)
572
+ return `View "${name}" not found. Use generate_view to create a new view instead.`;
573
+ return (
574
+ `Current configuration of view "${name}" (viewtemplate: ${existingView.viewtemplate}):\n` +
575
+ JSON.stringify(existingView.configuration, null, 2)
576
+ );
577
+ },
578
+ };
579
+
580
+ const applyViewConfigTool = {
581
+ type: "function",
582
+ function: {
583
+ name: "apply_view_config",
584
+ description:
585
+ "Save an updated configuration to an existing view. " +
586
+ "STRICT PRECONDITION: you must have already called get_view_config AND written out the complete merged configuration JSON before calling this tool. " +
587
+ "Do NOT call this tool as a placeholder or before the configuration is fully constructed. " +
588
+ "Calling this tool without a complete configuration object is always wrong and will fail.",
589
+ parameters: {
590
+ type: "object",
591
+ required: ["name", "configuration"],
592
+ properties: {
593
+ name: {
594
+ description: "The name of the existing view to update.",
595
+ type: "string",
596
+ },
597
+ configuration: {
598
+ type: "object",
599
+ description:
600
+ "REQUIRED. The complete updated configuration object — every key from the existing config preserved, with only your changes merged in. " +
601
+ "You MUST have the full object written out before calling this tool. " +
602
+ "Passing null, an empty object, or a partial object (e.g. only the name) is always wrong and will return an error.",
603
+ },
604
+ },
605
+ },
606
+ },
607
+ process: async ({ name, configuration }) => {
608
+ const existingView = View.findOne({ name });
609
+ if (!existingView) return `View "${name}" not found.`;
610
+ if (!configuration || typeof configuration !== "object")
611
+ return (
612
+ `ERROR: configuration is missing. ` +
613
+ `You must call get_view_config first, merge your changes into the full existing configuration, then call apply_view_config again with the complete configuration object.`
614
+ );
615
+ return { name, configuration, view_id: existingView.id };
616
+ },
617
+ postProcess: async ({ tool_call, req }) => {
618
+ const { name, configuration } = tool_call.input;
619
+ const existingView = View.findOne({ name });
620
+ if (!existingView)
621
+ return { stop: true, add_response: `View "${name}" not found.` };
622
+ if (!configuration || typeof configuration !== "object")
623
+ return {
624
+ stop: true,
625
+ add_response:
626
+ `apply_view_config called for "${name}" without a configuration object. ` +
627
+ `Call get_view_config first, merge your changes into the full existing configuration, then call apply_view_config again with the complete configuration.`,
628
+ };
629
+ const cfg = configuration;
630
+
631
+ if (this.yoloMode) {
632
+ await View.update({ configuration: cfg }, existingView.id);
633
+ setTimeout(() => getState().refresh_views(), 200);
634
+ return { stop: true, add_response: `View ${name} updated.` };
635
+ }
636
+ return {
637
+ stop: true,
638
+ add_response: pre(JSON.stringify(cfg, null, 2)),
639
+ add_user_action: {
640
+ name: "build_copilot_view_update",
641
+ type: "button",
642
+ label: "Save updated view " + name,
643
+ input: { name, configuration: cfg },
644
+ },
645
+ };
646
+ },
647
+ };
648
+
649
+ const getRelationPathsTool = {
650
+ type: "function",
651
+ function: GET_RELATION_PATHS_FUNCTION,
652
+ process: async ({ pairs }) => {
653
+ const schemaData = await build_schema_data();
654
+ const sections = getRelationPathsForPairs(pairs || [], schemaData);
655
+ return (
656
+ sections.join("\n\n") +
657
+ `\n\nFor each pair, set the "relation" property to one of the strings listed above.\n` +
658
+ `Pick by type: ChildList = multiple child rows, ParentShow = single parent, OneToOneShow = unique child. ` +
659
+ `If multiple paths of the same type exist, choose the one whose FK field name best matches the task. Prefer shorter paths.`
660
+ );
661
+ },
662
+ };
663
+
664
+ return [
665
+ generateViewTool,
666
+ getViewConfigTool,
667
+ applyViewConfigTool,
668
+ getRelationPathsTool,
669
+ ];
444
670
  };
445
671
  }
446
672
 
@@ -42,12 +42,14 @@ const existing_tables_list = (tables) => {
42
42
  tables.forEach((table) => {
43
43
  const fieldLines = table.fields.map(
44
44
  (f) =>
45
- ` * ${f.name} with type: ${f.pretty_type}.${f.description ? ` ${f.description}` : ""}`,
45
+ ` * ${f.name} with type: ${f.pretty_type}.${
46
+ f.description ? ` ${f.description}` : ""
47
+ }`
46
48
  );
47
49
  tableLines.push(
48
50
  `${table.name}${
49
51
  table.description ? `: ${table.description}.` : "."
50
- } Contains the following fields:\n${fieldLines.join("\n")}`,
52
+ } Contains the following fields:\n${fieldLines.join("\n")}`
51
53
  );
52
54
  });
53
55
  return `The database already contains the following tables:
@@ -55,4 +57,64 @@ const existing_tables_list = (tables) => {
55
57
  ${tableLines.join("\n\n")}`;
56
58
  };
57
59
 
58
- module.exports = { saltcorn_description, existing_tables_list };
60
+ const existing_entities_list = ({ views, triggers, pages, tableById = {} }) => {
61
+ const sections = [];
62
+ if (views.length)
63
+ sections.push(
64
+ `The following views are already implemented — do NOT plan tasks to create them:\n` +
65
+ views
66
+ .map((v) => {
67
+ const tablePart =
68
+ v.table?.name ||
69
+ (v.table_id && tableById[v.table_id]) ||
70
+ v.exttable_name;
71
+ return `- ${v.name} (${v.viewtemplate}${
72
+ tablePart ? ` on ${tablePart}` : ""
73
+ })`;
74
+ })
75
+ .join("\n")
76
+ );
77
+ if (triggers.length)
78
+ sections.push(
79
+ `The following triggers are already implemented — do NOT plan tasks to create them:\n` +
80
+ triggers
81
+ .map(
82
+ (t) =>
83
+ `- ${t.name} (${t.action}${
84
+ t.when_trigger ? `, ${t.when_trigger}` : ""
85
+ })`
86
+ )
87
+ .join("\n")
88
+ );
89
+ if (pages.length)
90
+ sections.push(
91
+ `The following pages are already implemented — do NOT plan tasks to create them:\n` +
92
+ pages.map((p) => `- ${p.name}`).join("\n")
93
+ );
94
+ return sections.join("\n\n");
95
+ };
96
+
97
+ const available_plugins_list = (storePlugins, installedNames) => {
98
+ const uninstalled = storePlugins.filter((p) => !installedNames.has(p.name));
99
+ if (!uninstalled.length) return "";
100
+ const lines = uninstalled.map((p) => {
101
+ let line = `### ${p.name}`;
102
+ if (p.description) line += `\n${p.description}`;
103
+ if (p.contents) line += `\n${p.contents}`;
104
+ return line;
105
+ });
106
+ return (
107
+ `The following plugins are available in the Saltcorn store but not yet installed. ` +
108
+ `If a task requires functionality provided by one of these plugins (e.g. a specific view template, field type, or action), ` +
109
+ `include an explicit "Install plugin <name>" task before it with the exact plugin name as listed here. ` +
110
+ `The executor will use that name directly without needing to look it up.\n\n` +
111
+ lines.join("\n\n")
112
+ );
113
+ };
114
+
115
+ module.exports = {
116
+ saltcorn_description,
117
+ existing_tables_list,
118
+ existing_entities_list,
119
+ available_plugins_list,
120
+ };
@@ -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
- } else {
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("No requirements found"),
88
- button(
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 gen_reqs = async (table_id, viewname, config, body, { req, res }) => {
100
- const spec = await MetaData.findOne({
139
+ const doGenReqs = async (spec, userId) => {
140
+ const generatingMd = await MetaData.create({
101
141
  type: "CopilotConstructMgr",
102
- name: "spec",
142
+ name: "generating_requirements",
143
+ body: {},
144
+ user_id: userId,
103
145
  });
104
- if (!spec) throw new Error("Specification not found");
105
- const answer = await getState().functions.llm_generate.run(
106
- `Generate the requirements for this application:
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
- tools: [requirements_tool],
118
- ...tool_choice("make_requirements"),
119
- systemPrompt:
120
- "You are a project manager. The user wants to build an application, and you must analyse their application description",
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
- const tc = answer.getToolCalls()[0];
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
- for (const reqm of tc.input.requirements)
127
- await MetaData.create({
128
- type: "CopilotConstructMgr",
129
- name: "requirement",
130
- body: reqm,
131
- user_id: req.user?.id,
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 };