@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.
@@ -1,364 +1,203 @@
1
- const Field = require("@saltcorn/data/models/field");
2
1
  const Table = require("@saltcorn/data/models/table");
3
- const Form = require("@saltcorn/data/models/form");
4
2
  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, tool_choice } = require("./common");
39
- const { requirements_tool } = require("./tools");
40
- const { saltcorn_description, existing_tables_list } = require("./prompts");
3
+ const { div, pre, p, small, span } = require("@saltcorn/markup/tags");
4
+ const { viewname } = require("./common");
41
5
  const GenerateTables = require("../actions/generate-tables");
42
6
  const { buildMermaidMarkup } = GenerateTables;
43
- const GenerateTablesSkill = require("../agent-skills/database-design");
44
7
 
45
- const showSchema = async (req) => {
46
- const schema = await MetaData.findOne({
47
- type: "CopilotConstructMgr",
48
- name: "schema",
49
- });
50
-
51
- if (schema) {
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
- : ""
132
- );
8
+ const PHASE_COLORS = [
9
+ { fill: "#93c5fd", stroke: "#1e40af" },
10
+ { fill: "#86efac", stroke: "#166534" },
11
+ { fill: "#fcd34d", stroke: "#92400e" },
12
+ { fill: "#d8b4fe", stroke: "#6b21a8" },
13
+ { fill: "#f9a8d4", stroke: "#9d174d" },
14
+ { fill: "#fdba74", stroke: "#9a3412" },
15
+ ];
16
+ const NO_PHASE_COLOR = { fill: "#cbd5e1", stroke: "#334155" };
133
17
 
134
- return div(
135
- { class: "mt-2" },
136
- pre({ class: "schema-mermaid" }, mmdia),
137
- colorScript,
138
- legend,
139
- !implemented &&
140
- div(
141
- { class: "mb-4 d-block mt-3" },
142
- button(
143
- {
144
- class: "btn btn-primary me-2",
145
- onclick: `view_post("${viewname}", "implement_schema")`,
146
- },
147
- "Implement schema"
148
- ),
149
- button(
150
- {
151
- class: "btn btn-outline-danger",
152
- onclick: `view_post("${viewname}", "del_schema")`,
153
- },
154
- "Delete schema"
155
- )
156
- )
157
- );
158
- }
159
-
160
- const generating = await MetaData.findOne({
161
- type: "CopilotConstructMgr",
162
- name: "generating_schema",
163
- });
164
- if (generating) {
18
+ const showSchema = async (req) => {
19
+ const allTables = await Table.find({});
20
+ const userTables = allTables.filter((t) => !t.name.startsWith("_sc_"));
21
+ if (!userTables.length) {
165
22
  return div(
166
23
  { class: "mt-2" },
167
24
  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
- `)
25
+ { class: "text-muted" },
26
+ "No tables in the database yet. Data model tasks in each phase will create them."
181
27
  )
182
28
  );
183
29
  }
184
30
 
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
- );
209
- };
210
-
211
- const doGenSchema = async (spec, rs, userId) => {
212
- const generatingMd = await MetaData.create({
31
+ const phaseRecords = await MetaData.find({
213
32
  type: "CopilotConstructMgr",
214
- name: "generating_schema",
215
- body: {},
216
- user_id: userId,
33
+ name: "table_phase",
217
34
  });
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:
223
-
224
- Description: ${spec.body.description}
225
- Audience: ${spec.body.audience}
226
- Core features: ${spec.body.core_features}
227
- Out of scope: ${spec.body.out_of_scope}
228
- Visual style: ${spec.body.visual_style}
229
-
230
- These are the requirements of the application:
231
-
232
- ${rs.map((r) => `* ${r.body.requirement}`).join("\n")}
233
-
234
- ${saltcorn_description}
235
-
236
- ${existing_tables_list(existing_tables)}
237
-
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.
239
-
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.
35
+ const tablePhaseMap = {};
36
+ for (const r of phaseRecords) tablePhaseMap[r.body.table_name] = r.body;
37
+
38
+ // Collect phases seen, sorted by index
39
+ const phasesSeen = new Map();
40
+ for (const r of phaseRecords)
41
+ if (!phasesSeen.has(r.body.phase_idx))
42
+ phasesSeen.set(r.body.phase_idx, r.body.phase_name);
43
+ const orderedPhases = [...phasesSeen.entries()].sort(([a], [b]) => a - b);
44
+
45
+ // Group table names by phase for the legend
46
+ const phaseTableNames = new Map();
47
+ const unassignedNames = [];
48
+ for (const t of userTables) {
49
+ const entry = tablePhaseMap[t.name];
50
+ if (entry) {
51
+ if (!phaseTableNames.has(entry.phase_idx))
52
+ phaseTableNames.set(entry.phase_idx, []);
53
+ phaseTableNames.get(entry.phase_idx).push(t.name);
54
+ } else {
55
+ unassignedNames.push(t.name);
56
+ }
57
+ }
243
58
 
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.
245
- For every field that must not be empty, set not_null=true.
246
- Do NOT leave uniqueness or required constraints for a later step express them fully in this schema.
59
+ // Pre-compute fill color per table name for the client script
60
+ const colorMap = {};
61
+ for (const t of userTables) {
62
+ const entry = tablePhaseMap[t.name];
63
+ colorMap[t.name] = entry
64
+ ? PHASE_COLORS[entry.phase_idx % PHASE_COLORS.length]
65
+ : NO_PHASE_COLOR;
66
+ }
247
67
 
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.
68
+ const mmdia = buildMermaidMarkup(userTables);
249
69
 
250
- Now use the ${
251
- databaseDesignTool.function.name
252
- } tool to generate the complete database schema for this software application
253
- `,
70
+ const tableBadge = (color, name) =>
71
+ span(
254
72
  {
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
- }
73
+ class: "badge rounded-pill",
74
+ style: `background:${color.fill};color:${color.stroke};border:1.5px solid ${color.stroke}`,
75
+ },
76
+ name
260
77
  );
261
78
 
262
- const tc = answer.getToolCalls()[0];
263
-
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
- });
271
-
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({
288
- type: "CopilotConstructMgr",
289
- name: "spec",
290
- });
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");
79
+ const legendGroups = [
80
+ ...(unassignedNames.length
81
+ ? [
82
+ div(
83
+ { class: "d-flex flex-wrap align-items-center gap-1" },
84
+ span({ class: "me-1 text-muted small" }, "Pre-existing:"),
85
+ ...unassignedNames.map((n) => tableBadge(NO_PHASE_COLOR, n))
86
+ ),
87
+ ]
88
+ : []),
89
+ ...orderedPhases
90
+ .map(([idx, name]) => {
91
+ const color = PHASE_COLORS[idx % PHASE_COLORS.length];
92
+ const names = phaseTableNames.get(idx) || [];
93
+ if (!names.length) return null;
94
+ return div(
95
+ { class: "d-flex flex-wrap align-items-center gap-1" },
96
+ span(
97
+ { class: "me-1 text-muted small" },
98
+ `Phase ${idx + 1}${name ? `: ${name}` : ""}:`
99
+ ),
100
+ ...names.map((n) => tableBadge(color, n))
101
+ );
102
+ })
103
+ .filter(Boolean),
104
+ ];
297
105
 
298
- doGenSchema(spec, rs, req.user?.id).catch((e) =>
299
- console.error("gen_schema error", e)
106
+ return div(
107
+ { class: "mt-2" },
108
+ small(
109
+ { class: "text-muted d-block mb-2" },
110
+ `${userTables.length} table${
111
+ userTables.length !== 1 ? "s" : ""
112
+ } — reflects current database state`
113
+ ),
114
+ pre(
115
+ {
116
+ class: "schema-mermaid",
117
+ "data-color-map": JSON.stringify(colorMap),
118
+ },
119
+ mmdia
120
+ ),
121
+ legendGroups.length
122
+ ? div({ class: "d-flex flex-column gap-2 mt-2" }, ...legendGroups)
123
+ : ""
300
124
  );
301
- return { json: { success: true } };
302
125
  };
303
126
 
304
- const schema_status = async (
127
+ const schema_list_html = async (
305
128
  table_id,
306
129
  viewname,
307
130
  config,
308
131
  body,
309
132
  { req, res }
310
133
  ) => {
311
- const generating = await MetaData.findOne({
312
- type: "CopilotConstructMgr",
313
- name: "generating_schema",
314
- });
315
- return { json: { generating: !!generating } };
134
+ const html = await showSchema(req);
135
+ return { json: { html } };
316
136
  };
317
137
 
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
- }
323
- return { json: { reload_page: true } };
138
+ const schema_routes = {
139
+ schema_list_html,
324
140
  };
325
141
 
326
- const implement_schema = async (
327
- table_id,
328
- viewname,
329
- config,
330
- body,
331
- { req, res }
332
- ) => {
333
- const md = await MetaData.findOne({
334
- type: "CopilotConstructMgr",
335
- name: "schema",
336
- });
142
+ const schemaStaticScript = `<script>
143
+ (function(){
144
+ const _schemVn = ${JSON.stringify(viewname)};
337
145
 
338
- const { apply_copilot_tables } = new GenerateTablesSkill({}).userActions;
339
- const existingNames = new Set((await Table.find({})).map((t) => t.name));
340
- const newTables = md.body.tables.filter((t) => {
341
- if (existingNames.has(t.name)) {
342
- getState().log(
343
- 2,
344
- `AppConstructor: skipping table "${t.name}" — already exists in database`
345
- );
346
- return false;
146
+ // Ensure mermaid won't auto-process on its own — we drive rendering manually
147
+ if (typeof mermaid !== 'undefined') {
148
+ try { mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' }); } catch (_) {}
149
+ }
150
+
151
+ function applyColors(el, colorMap) {
152
+ for (const g of el.querySelectorAll('g[id^="entity-"]')) {
153
+ const name = g.id.replace(/^entity-/, '').replace(/-\\d+$/, '');
154
+ const colors = colorMap[name];
155
+ if (!colors) continue;
156
+ const path = g.querySelector('path');
157
+ if (path) {
158
+ path.style.fill = colors.fill;
159
+ path.style.stroke = colors.stroke;
160
+ }
347
161
  }
348
- return true;
349
- });
350
- await apply_copilot_tables({ tables: newTables, user: req.user });
351
- md.body.implemented = true;
352
- await md.update({ body: md.body });
162
+ }
353
163
 
354
- return { json: { reload_page: true } };
355
- };
164
+ window.copilotRenderSchemaMermaid = () => {
165
+ const pre = document.querySelector('#schema-list-area .schema-mermaid');
166
+ if (!pre || typeof mermaid === 'undefined') return;
167
+ const colorMap = pre.dataset.colorMap ? JSON.parse(pre.dataset.colorMap) : {};
168
+ const mermaidText = pre.textContent.trim();
169
+ if (!mermaidText) return;
170
+
171
+ const tmp = document.createElement('div');
172
+ tmp.style.cssText = 'position:absolute;left:-9999px;top:0;visibility:hidden';
173
+ tmp.textContent = mermaidText;
174
+ document.body.appendChild(tmp);
175
+
176
+ mermaid.run({ nodes: [tmp], suppressErrors: true })
177
+ .then(() => {
178
+ const target = document.querySelector('#schema-list-area .schema-mermaid');
179
+ if (target) { target.innerHTML = tmp.innerHTML; applyColors(target, colorMap); }
180
+ tmp.remove();
181
+ })
182
+ .catch(e => {
183
+ console.warn('mermaid schema render error', e);
184
+ tmp.remove();
185
+ });
186
+ };
356
187
 
357
- const schema_routes = {
358
- gen_schema,
359
- schema_status,
360
- del_schema,
361
- implement_schema,
362
- };
188
+ window.copilotRefreshSchema = function() {
189
+ view_post(_schemVn, 'schema_list_html', {}, (r) => {
190
+ const el = document.getElementById('schema-list-area');
191
+ if (r && r.html && el) {
192
+ el.innerHTML = r.html;
193
+ copilotRenderSchemaMermaid();
194
+ }
195
+ });
196
+ };
197
+
198
+ if (document.readyState !== 'loading') copilotRenderSchemaMermaid();
199
+ else document.addEventListener('DOMContentLoaded', copilotRenderSchemaMermaid);
200
+ })()
201
+ </script>`;
363
202
 
364
- module.exports = { showSchema, schema_routes };
203
+ module.exports = { showSchema, schema_routes, schemaStaticScript };