@saltcorn/copilot 0.8.1 → 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,199 +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
- const GenerateTablesSkill = require("../agent-skills/database-design");
6
+ const { buildMermaidMarkup } = GenerateTables;
7
+
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" };
43
17
 
44
18
  const showSchema = async (req) => {
45
- const schema = await MetaData.findOne({
46
- type: "CopilotConstructMgr",
47
- name: "schema",
48
- });
49
-
50
- if (schema) {
51
- const preview = GenerateTables.render_html(
52
- { tables: schema.body.tables },
53
- true
54
- );
55
-
56
- return div(
57
- { class: "mt-2" },
58
- preview,
59
- !schema.body.implemented &&
60
- div(
61
- { class: "mb-4 d-block mt-3" },
62
- button(
63
- {
64
- class: "btn btn-primary me-2",
65
- onclick: `view_post("${viewname}", "implement_schema")`,
66
- },
67
- "Implement schema"
68
- ),
69
- button(
70
- {
71
- class: "btn btn-outline-danger",
72
- onclick: `view_post("${viewname}", "del_schema")`,
73
- },
74
- "Delete schema"
75
- )
76
- )
77
- );
78
- } else {
19
+ const allTables = await Table.find({});
20
+ const userTables = allTables.filter((t) => !t.name.startsWith("_sc_"));
21
+ if (!userTables.length) {
79
22
  return div(
80
23
  { 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"
24
+ p(
25
+ { class: "text-muted" },
26
+ "No tables in the database yet. Data model tasks in each phase will create them."
88
27
  )
89
28
  );
90
29
  }
91
- };
92
30
 
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({
31
+ const phaseRecords = await MetaData.find({
100
32
  type: "CopilotConstructMgr",
101
- name: "requirement",
33
+ name: "table_phase",
102
34
  });
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:
109
-
110
- Description: ${spec.body.description}
111
- Audience: ${spec.body.audience}
112
- Core features: ${spec.body.core_features}
113
- Out of scope: ${spec.body.out_of_scope}
114
- Visual style: ${spec.body.visual_style}
115
-
116
- These are the requirements of the application:
117
-
118
- ${rs.map((r) => `* ${r.body.requirement}`).join("\n")}
119
-
120
- ${saltcorn_description}
121
-
122
- ${existing_tables_list(existing_tables)}
123
-
124
- 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.
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
+ }
125
58
 
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.
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
+ }
127
67
 
128
- 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
- For every field that must not be empty, set not_null=true.
130
- Do NOT leave uniqueness or required constraints for a later step — express them fully in this schema.
68
+ const mmdia = buildMermaidMarkup(userTables);
131
69
 
132
- 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.
70
+ const tableBadge = (color, name) =>
71
+ span(
72
+ {
73
+ class: "badge rounded-pill",
74
+ style: `background:${color.fill};color:${color.stroke};border:1.5px solid ${color.stroke}`,
75
+ },
76
+ name
77
+ );
133
78
 
134
- Now use the ${
135
- databaseDesignTool.function.name
136
- } tool to generate the complete database schema for this software application
137
- `,
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
- }
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
+ ];
105
+
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
+ : ""
144
124
  );
145
-
146
- const tc = answer.getToolCalls()[0];
147
-
148
- await MetaData.create({
149
- type: "CopilotConstructMgr",
150
- name: "schema",
151
- body: { tables: tc.input.tables, implemented: false },
152
- user_id: req.user?.id,
153
- });
154
- return { json: { reload_page: true } };
155
- };
156
-
157
- const del_schema = async (table_id, viewname, config, body, { req, res }) => {
158
- const rs = await MetaData.find({
159
- type: "CopilotConstructMgr",
160
- name: "schema",
161
- });
162
- for (const r of rs) await r.delete();
163
- return { json: { reload_page: true } };
164
125
  };
165
126
 
166
- const implement_schema = async (
127
+ const schema_list_html = async (
167
128
  table_id,
168
129
  viewname,
169
130
  config,
170
131
  body,
171
132
  { req, res }
172
133
  ) => {
173
- const md = await MetaData.findOne({
174
- type: "CopilotConstructMgr",
175
- name: "schema",
176
- });
177
-
178
- const { apply_copilot_tables } = new GenerateTablesSkill({}).userActions;
179
- const existingNames = new Set((await Table.find({})).map((t) => t.name));
180
- const newTables = md.body.tables.filter((t) => {
181
- if (existingNames.has(t.name)) {
182
- getState().log(
183
- 2,
184
- `AppConstructor: skipping table "${t.name}" — already exists in database`
185
- );
186
- return false;
187
- }
188
- return true;
189
- });
190
- await apply_copilot_tables({ tables: newTables, user: req.user });
191
- md.body.implemented = true;
192
- await md.update({ body: md.body });
134
+ const html = await showSchema(req);
135
+ return { json: { html } };
136
+ };
193
137
 
194
- return { json: { reload_page: true } };
138
+ const schema_routes = {
139
+ schema_list_html,
195
140
  };
196
141
 
197
- const schema_routes = { gen_schema, del_schema, implement_schema };
142
+ const schemaStaticScript = `<script>
143
+ (function(){
144
+ const _schemVn = ${JSON.stringify(viewname)};
145
+
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
+ }
161
+ }
162
+ }
198
163
 
199
- module.exports = { showSchema, schema_routes };
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
+ };
187
+
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>`;
202
+
203
+ module.exports = { showSchema, schema_routes, schemaStaticScript };