@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.
@@ -39,6 +39,7 @@ const { viewname, tool_choice } = require("./common");
39
39
  const { requirements_tool } = require("./tools");
40
40
  const { saltcorn_description, existing_tables_list } = require("./prompts");
41
41
  const GenerateTables = require("../actions/generate-tables");
42
+ const { buildMermaidMarkup } = GenerateTables;
42
43
  const GenerateTablesSkill = require("../agent-skills/database-design");
43
44
 
44
45
  const showSchema = async (req) => {
@@ -48,15 +49,94 @@ const showSchema = async (req) => {
48
49
  });
49
50
 
50
51
  if (schema) {
51
- const preview = GenerateTables.render_html(
52
- { tables: schema.body.tables },
53
- true
52
+ const newTableDefs = schema.body.tables || [];
53
+ const newTableInstances = GenerateTables.process_tables(newTableDefs);
54
+ const reusedMd = await MetaData.findOne({
55
+ type: "CopilotConstructMgr",
56
+ name: "reused_schema",
57
+ });
58
+ const reusedNames = reusedMd?.body?.table_names || [];
59
+ const reusedInstances = reusedNames
60
+ .map((n) => Table.findOne({ name: n }))
61
+ .filter(Boolean);
62
+ const allTables = [...newTableInstances, ...reusedInstances];
63
+ const mmdia = buildMermaidMarkup(allTables);
64
+ const implemented = !!schema.body.implemented;
65
+
66
+ const newNames = newTableDefs.map((t) => t.table_name).filter(Boolean);
67
+
68
+ const colorMap = Object.fromEntries([
69
+ ...newNames.map((n) => [n, "#198754"]),
70
+ ...reusedNames.map((n) => [n, "#6c757d"]),
71
+ ]);
72
+ const colorScript = script(
73
+ domReady(`
74
+ const colors = ${JSON.stringify(colorMap)};
75
+ const pre = document.querySelector('.schema-mermaid');
76
+ if (!pre) return;
77
+
78
+ const doRender = () => {
79
+ mermaid.run({ nodes: [pre], suppressErrors: true, postRenderCallback: () => {
80
+ for (const g of pre.querySelectorAll('g[id^="entity-"]')) {
81
+ const name = g.id.replace(/^entity-/, '').replace(/-\\d+$/, '');
82
+ const color = colors[name];
83
+ if (color) {
84
+ const p = g.querySelector('path');
85
+ if (p) p.setAttribute('fill', color);
86
+ }
87
+ }
88
+ for (const el of pre.querySelectorAll('g.label.name .nodeLabel')) {
89
+ el.style.color = 'white';
90
+ el.style.fontWeight = 'bold';
91
+ }
92
+ }});
93
+ };
94
+
95
+ // Defer render until tab is visible, then colorize nodes.
96
+ const pane = pre.closest('.tab-pane');
97
+ if (pane && !pane.classList.contains('active')) {
98
+ const link = document.querySelector('[href="#' + pane.id + '"]');
99
+ if (link) link.addEventListener('shown.bs.tab', doRender, { once: true });
100
+ else {
101
+ const o = new MutationObserver(() => {
102
+ if (pane.classList.contains('active')) { o.disconnect(); doRender(); }
103
+ });
104
+ o.observe(pane, { attributes: true, attributeFilter: ['class'] });
105
+ }
106
+ } else doRender();
107
+ `)
108
+ );
109
+
110
+ const legend = div(
111
+ { class: "mt-3 d-flex flex-wrap gap-3 align-items-start" },
112
+ newNames.length
113
+ ? div(
114
+ { class: "d-flex flex-wrap align-items-center gap-1" },
115
+ span(
116
+ { class: "me-1 text-muted small" },
117
+ implemented ? "Was created:" : "Will be created:"
118
+ ),
119
+ ...newNames.map((n) => span({ class: "badge bg-success" }, n))
120
+ )
121
+ : "",
122
+ reusedNames.length
123
+ ? div(
124
+ { class: "d-flex flex-wrap align-items-center gap-1" },
125
+ span(
126
+ { class: "me-1 text-muted small" },
127
+ implemented ? "Already existed:" : "Already exists:"
128
+ ),
129
+ ...reusedNames.map((n) => span({ class: "badge bg-secondary" }, n))
130
+ )
131
+ : ""
54
132
  );
55
133
 
56
134
  return div(
57
135
  { class: "mt-2" },
58
- preview,
59
- !schema.body.implemented &&
136
+ pre({ class: "schema-mermaid" }, mmdia),
137
+ colorScript,
138
+ legend,
139
+ !implemented &&
60
140
  div(
61
141
  { class: "mb-4 d-block mt-3" },
62
142
  button(
@@ -75,37 +155,71 @@ const showSchema = async (req) => {
75
155
  )
76
156
  )
77
157
  );
78
- } else {
158
+ }
159
+
160
+ const generating = await MetaData.findOne({
161
+ type: "CopilotConstructMgr",
162
+ name: "generating_schema",
163
+ });
164
+ if (generating) {
79
165
  return div(
80
166
  { class: "mt-2" },
81
- p("Schema not found"),
82
- button(
83
- {
84
- class: "btn btn-primary",
85
- onclick: `press_store_button(this);view_post("${viewname}", "gen_schema")`,
86
- },
87
- "Generate schema"
167
+ p(
168
+ i({ class: "fas fa-spinner fa-spin me-2" }),
169
+ "Generating schema, please wait..."
170
+ ),
171
+ script(
172
+ domReady(`
173
+ const poll = () => {
174
+ view_post(${JSON.stringify(viewname)}, 'schema_status', {}, (resp) => {
175
+ if (resp && !resp.generating) location.reload();
176
+ else setTimeout(poll, 3000);
177
+ });
178
+ };
179
+ setTimeout(poll, 3000);
180
+ `)
88
181
  )
89
182
  );
90
183
  }
184
+
185
+ return div(
186
+ { class: "mt-2", id: "schema-gen-area" },
187
+ p("Schema not found"),
188
+ button(
189
+ { class: "btn btn-primary", onclick: `copilotGenSchema()` },
190
+ "Generate schema"
191
+ ),
192
+ script(
193
+ domReady(`
194
+ window.copilotGenSchema = () => {
195
+ document.getElementById('schema-gen-area').innerHTML =
196
+ '<p><i class="fas fa-spinner fa-spin me-2"></i>Generating schema, please wait...</p>';
197
+ view_post(${JSON.stringify(viewname)}, 'gen_schema', {}, () => {});
198
+ const poll = () => {
199
+ view_post(${JSON.stringify(viewname)}, 'schema_status', {}, (resp) => {
200
+ if (resp && !resp.generating) location.reload();
201
+ else setTimeout(poll, 3000);
202
+ });
203
+ };
204
+ setTimeout(poll, 3000);
205
+ };
206
+ `)
207
+ )
208
+ );
91
209
  };
92
210
 
93
- const gen_schema = async (table_id, viewname, config, body, { req, res }) => {
94
- const spec = await MetaData.findOne({
95
- type: "CopilotConstructMgr",
96
- name: "spec",
97
- });
98
- if (!spec) throw new Error("Specification not found");
99
- const rs = await MetaData.find({
211
+ const doGenSchema = async (spec, rs, userId) => {
212
+ const generatingMd = await MetaData.create({
100
213
  type: "CopilotConstructMgr",
101
- name: "requirement",
214
+ name: "generating_schema",
215
+ body: {},
216
+ user_id: userId,
102
217
  });
103
- if (!rs.length) throw new Error("No requirements found");
104
-
105
- const databaseDesignTool = new GenerateTablesSkill({}).provideTools();
106
- const existing_tables = await Table.find({});
107
- const answer = await getState().functions.llm_generate.run(
108
- `Generate the database schema for this application:
218
+ try {
219
+ const databaseDesignTool = new GenerateTablesSkill({}).provideTools();
220
+ const existing_tables = await Table.find({});
221
+ const answer = await getState().functions.llm_generate.run(
222
+ `Generate the database schema for this application:
109
223
 
110
224
  Description: ${spec.body.description}
111
225
  Audience: ${spec.body.audience}
@@ -113,7 +227,7 @@ Core features: ${spec.body.core_features}
113
227
  Out of scope: ${spec.body.out_of_scope}
114
228
  Visual style: ${spec.body.visual_style}
115
229
 
116
- These are the requirements of the application:
230
+ These are the requirements of the application:
117
231
 
118
232
  ${rs.map((r) => `* ${r.body.requirement}`).join("\n")}
119
233
 
@@ -123,7 +237,9 @@ ${existing_tables_list(existing_tables)}
123
237
 
124
238
  Design a complete database schema that covers ALL requirements listed above. Every distinct entity in the application must have its own table. Do not produce a minimal or partial schema — all tables needed to implement every requirement must be included in this single call. Do not leave any tables for a later step.
125
239
 
126
- The tables listed above are already implemented in the database include them in the schema as-is so the full data model is visible, but do not change their fields. Only add new tables for entities not yet covered. The implementation step will skip any table whose name already exists.
240
+ The tables listed above already exist in the database. Do NOT modify or extend them treat them as fixed. Handle them as follows:
241
+ - If an existing table is already complete and used as-is: add its name to reused_table_names. Do NOT define its fields again in the tables array.
242
+ - New tables not yet in the database: include them in the tables array with all their fields as usual.
127
243
 
128
244
  For every field that must be unique (e.g. unique email, unique slug, unique combination keys expressed as individual unique fields), set unique=true on that field.
129
245
  For every field that must not be empty, set not_null=true.
@@ -132,34 +248,78 @@ Do NOT leave uniqueness or required constraints for a later step — express the
132
248
  Note: ownership configuration (automatically populating a FK-to-users field from the logged-in user) is a VIEW-level concern and cannot be expressed in the schema. Do not attempt to annotate fields as "ownership fields" here — simply define the foreign key field normally. Ownership will be configured when the Edit views are generated.
133
249
 
134
250
  Now use the ${
135
- databaseDesignTool.function.name
136
- } tool to generate the complete database schema for this software application
251
+ databaseDesignTool.function.name
252
+ } tool to generate the complete database schema for this software application
137
253
  `,
138
- {
139
- tools: [databaseDesignTool],
140
- ...tool_choice(databaseDesignTool.function.name),
141
- systemPrompt:
142
- "You are a database designer. The user wants to build an application, and you must analyse their application description and requirements and design a complete schema. Every entity needed by any requirement must have its own table. Never produce a partial schema.",
143
- }
144
- );
254
+ {
255
+ tools: [databaseDesignTool],
256
+ ...tool_choice(databaseDesignTool.function.name),
257
+ systemPrompt:
258
+ "You are a database designer. The user wants to build an application, and you must analyse their application description and requirements and design a complete schema. Every entity needed by any requirement must have its own table. Never produce a partial schema.",
259
+ }
260
+ );
261
+
262
+ const tc = answer.getToolCalls()[0];
145
263
 
146
- const tc = answer.getToolCalls()[0];
264
+ const noNewTables = !tc.input.tables || tc.input.tables.length === 0;
265
+ await MetaData.create({
266
+ type: "CopilotConstructMgr",
267
+ name: "schema",
268
+ body: { tables: tc.input.tables || [], implemented: noNewTables },
269
+ user_id: userId,
270
+ });
147
271
 
148
- await MetaData.create({
272
+ const reusedNames = tc.input.reused_table_names || [];
273
+ if (reusedNames.length) {
274
+ await MetaData.create({
275
+ type: "CopilotConstructMgr",
276
+ name: "reused_schema",
277
+ body: { table_names: reusedNames },
278
+ user_id: userId,
279
+ });
280
+ }
281
+ } finally {
282
+ await generatingMd.delete();
283
+ }
284
+ };
285
+
286
+ const gen_schema = async (table_id, viewname, config, body, { req, res }) => {
287
+ const spec = await MetaData.findOne({
149
288
  type: "CopilotConstructMgr",
150
- name: "schema",
151
- body: { tables: tc.input.tables, implemented: false },
152
- user_id: req.user?.id,
289
+ name: "spec",
153
290
  });
154
- return { json: { reload_page: true } };
291
+ if (!spec) throw new Error("Specification not found");
292
+ const rs = await MetaData.find({
293
+ type: "CopilotConstructMgr",
294
+ name: "requirement",
295
+ });
296
+ if (!rs.length) throw new Error("No requirements found");
297
+
298
+ doGenSchema(spec, rs, req.user?.id).catch((e) =>
299
+ console.error("gen_schema error", e)
300
+ );
301
+ return { json: { success: true } };
155
302
  };
156
303
 
157
- const del_schema = async (table_id, viewname, config, body, { req, res }) => {
158
- const rs = await MetaData.find({
304
+ const schema_status = async (
305
+ table_id,
306
+ viewname,
307
+ config,
308
+ body,
309
+ { req, res }
310
+ ) => {
311
+ const generating = await MetaData.findOne({
159
312
  type: "CopilotConstructMgr",
160
- name: "schema",
313
+ name: "generating_schema",
161
314
  });
162
- for (const r of rs) await r.delete();
315
+ return { json: { generating: !!generating } };
316
+ };
317
+
318
+ const del_schema = async (table_id, viewname, config, body, { req, res }) => {
319
+ for (const name of ["schema", "reused_schema", "generating_schema"]) {
320
+ const rs = await MetaData.find({ type: "CopilotConstructMgr", name });
321
+ for (const r of rs) await r.delete();
322
+ }
163
323
  return { json: { reload_page: true } };
164
324
  };
165
325
 
@@ -194,6 +354,11 @@ const implement_schema = async (
194
354
  return { json: { reload_page: true } };
195
355
  };
196
356
 
197
- const schema_routes = { gen_schema, del_schema, implement_schema };
357
+ const schema_routes = {
358
+ gen_schema,
359
+ schema_status,
360
+ del_schema,
361
+ implement_schema,
362
+ };
198
363
 
199
364
  module.exports = { showSchema, schema_routes };