@saltcorn/bootstrap-prompt-theme 0.1.2 → 0.1.4

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.
package/README.md CHANGED
@@ -30,6 +30,18 @@ This variant generates a lightweight CSS overlay by chatting with an LLM. Bootst
30
30
 
31
31
  The chat history is persisted across page reloads, so you can return to the configuration page and pick up where you left off.
32
32
 
33
+ ### Visual feedback
34
+
35
+ If the `@saltcorn/page-to-pdf` plugin is installed, the AI can capture a screenshot of a live page after each CSS change and use it to self-correct. To enable this, mention a page name in your message:
36
+
37
+ ```
38
+ You: A dark cyberpunk theme. Use the "dashboard" page for visual feedback.
39
+
40
+ AI: [applies CSS overlay, captures screenshot, refines if needed]
41
+ ```
42
+
43
+ The page name is remembered for the rest of the conversation — you only need to mention it once. The AI will tell you whether a screenshot was received and used for refinement, or explain if it failed.
44
+
33
45
  ### Example conversation
34
46
 
35
47
  ```
package/agent-skill.js CHANGED
@@ -2,8 +2,8 @@ const Plugin = require("@saltcorn/data/models/plugin");
2
2
  const { pre, code, div } = require("@saltcorn/markup/tags");
3
3
  const db = require("@saltcorn/data/db");
4
4
  const { getState } = require("@saltcorn/data/db/state");
5
- const { join } = require("path");
6
5
  const { writeFile, readdir, unlink } = require("fs").promises;
6
+ const { join } = require("path");
7
7
 
8
8
  const writeOverlayCSS = async (css, filename = `overlay.${Date.now()}.css`) => {
9
9
  const dest = join(__dirname, "public", filename);
@@ -67,11 +67,56 @@ LAYOUT CONFIG: Besides CSS, you can control structural layout options via set_la
67
67
  - top_pad: 0–5 — Bootstrap spacing scale for top padding on page sections
68
68
  - in_card: true | false — wrap page body in a Bootstrap card
69
69
  Only call set_layout_config when you want to change structural layout, separate from CSS. Call both tools when a request requires both structural and CSS changes.
70
+ When you change menu_style, the tool response will include a page_structure field with rendered HTML of the new layout. Read it to understand the actual element hierarchy and CSS selectors before writing any CSS for that layout.
71
+
72
+ {{PAGE_STRUCTURE}}
70
73
 
71
74
  WORKFLOW:
72
75
  1. Call apply_css_overlay with the complete CSS — this is the only way to deliver CSS.
73
76
  2. Optionally call set_layout_config for structural layout changes.
74
- 3. After all tool calls, reply with one short sentence confirming what changed. Never include CSS in your text reply.`;
77
+ 3. After apply_css_overlay completes, the tool result JSON may contain a "screenshot" field — a base64-encoded PNG of the live page captured after the CSS was applied. Screenshots are only available when a reference page has been configured (via the "route" parameter) and the environment supports it; if the field is absent or null, skip to step 4 immediately. To read the image, interpret the "screenshot" value as a base64 PNG: decode it visually and assess the rendered page. The "route" parameter only needs to be passed the first time or when the user changes the reference page — the value is stored and reused automatically.
78
+ 4. If a screenshot is present, review it: check that colors, fonts, spacing, and contrast match the intended design. If there is a clear problem, call apply_css_overlay with a targeted correction and review the next screenshot. Repeat only while there are clear remaining issues — stop as soon as the result looks good or after at most 3 correction passes, whichever comes first.
79
+ 5. Once done, reply with one short sentence confirming what changed. Then report on the screenshot status — be specific, not generic:
80
+ - If a screenshot was received and used for refinement: say which page was used and that visual feedback was applied.
81
+ - If a route was configured but the screenshot field was absent or null: say that the screenshot for that page failed or returned no data.
82
+ - If no route was configured at all: add a short note that the user can specify a page name via the "route" parameter in their next request to enable visual feedback.
83
+ Never include CSS in your text reply.`;
84
+
85
+ const capturePageScreenshot = async (req) => {
86
+ const plugin = await findPlugin();
87
+ const pageName = plugin?.configuration?.screenshot_page;
88
+ if (!pageName) return null;
89
+ const action = getState().actions?.page_to_pdf;
90
+ if (!action) {
91
+ getState().log(
92
+ 5,
93
+ "bootstrap-prompt-theme: page_to_pdf action not available, skipping screenshot"
94
+ );
95
+ return null;
96
+ }
97
+ try {
98
+ const base =
99
+ req?.protocol && req?.get?.("host")
100
+ ? `${req.protocol}://${req.get("host")}`
101
+ : getState().getConfig("base_url", "").replace(/\/$/, "");
102
+ const result = await action.run({
103
+ req,
104
+ referrer: base + "/",
105
+ configuration: {
106
+ entity_type: "URL",
107
+ url: `${base}/page/${pageName}`,
108
+ format: "PNG",
109
+ },
110
+ });
111
+ return result?.download?.blob || null;
112
+ } catch (e) {
113
+ getState().log(
114
+ 4,
115
+ `bootstrap-prompt-theme: screenshot failed: ${e.message}`
116
+ );
117
+ return null;
118
+ }
119
+ };
75
120
 
76
121
  const findPlugin = async () => {
77
122
  return (
@@ -89,6 +134,18 @@ const savePluginConfig = async (plugin, patch) => {
89
134
  });
90
135
  };
91
136
 
137
+ const renderStructureSkeleton = (config) => {
138
+ try {
139
+ return require(join(__dirname, "index.js")).renderStructureSkeleton(config);
140
+ } catch (e) {
141
+ getState().log(
142
+ 4,
143
+ `bootstrap-prompt-theme: renderStructureSkeleton failed: ${e.message}`
144
+ );
145
+ return null;
146
+ }
147
+ };
148
+
92
149
  class GenerateBootstrapThemeSkill {
93
150
  static skill_name = "Generate Bootstrap Theme";
94
151
 
@@ -104,8 +161,26 @@ class GenerateBootstrapThemeSkill {
104
161
  return [];
105
162
  }
106
163
 
107
- systemPrompt() {
108
- return SYSTEM_PROMPT;
164
+ async systemPrompt() {
165
+ const plugin = await findPlugin();
166
+ const config = plugin?.configuration || {};
167
+ let pageStructure =
168
+ "PAGE STRUCTURE: (unavailable — could not render current layout)";
169
+ try {
170
+ const { renderPageSkeleton } = require(join(__dirname, "index.js"));
171
+ const html = renderPageSkeleton(config);
172
+ if (html)
173
+ pageStructure =
174
+ "PAGE STRUCTURE: The current page renders like this (with a sample list view, based on the active layout config). Use it to understand element hierarchy, class names, and selectors when writing CSS:\n```html\n" +
175
+ html +
176
+ "\n```";
177
+ } catch (e) {
178
+ getState().log(
179
+ 4,
180
+ `bootstrap-prompt-theme: systemPrompt renderPageSkeleton failed: ${e.message}`
181
+ );
182
+ }
183
+ return SYSTEM_PROMPT.replace("{{PAGE_STRUCTURE}}", pageStructure);
109
184
  }
110
185
 
111
186
  provideTools() {
@@ -117,17 +192,17 @@ class GenerateBootstrapThemeSkill {
117
192
  code(css.slice(0, 300) + (css.length > 300 ? "\n..." : ""))
118
193
  );
119
194
  },
120
- process: async ({ css }) => {
195
+ process: async ({ css, route }, { req }) => {
121
196
  const filename = await writeOverlayCSS(css);
122
197
  const plugin = await findPlugin();
123
198
  if (plugin) {
124
- await savePluginConfig(plugin, {
125
- overlay_css: css,
126
- overlay_file: filename,
127
- });
199
+ const patch = { overlay_css: css, overlay_file: filename };
200
+ if (route) patch.screenshot_page = route;
201
+ await savePluginConfig(plugin, patch);
128
202
  await deleteOldOverlays(filename);
129
203
  }
130
- return { filename };
204
+ const screenshot = await capturePageScreenshot(req);
205
+ return { filename, screenshot };
131
206
  },
132
207
  renderToolResponse: async ({ filename }) => {
133
208
  return div(
@@ -148,6 +223,11 @@ class GenerateBootstrapThemeSkill {
148
223
  description:
149
224
  "Complete valid CSS to write as the overlay. Must start with :root { or a comment. No markdown, no code fences.",
150
225
  },
226
+ route: {
227
+ type: "string",
228
+ description:
229
+ "Saltcorn page name to use for visual feedback screenshots. Only pass this when the user has explicitly named a page. Do not guess or default to any page name. Once set, the value is remembered — only pass it again when the user specifies a different page.",
230
+ },
151
231
  },
152
232
  },
153
233
  },
@@ -162,30 +242,41 @@ class GenerateBootstrapThemeSkill {
162
242
  return pre(code(entries));
163
243
  },
164
244
  process: async (params) => {
245
+ const allowed = [
246
+ "mode",
247
+ "menu_style",
248
+ "colorscheme",
249
+ "fixed_top",
250
+ "fluid",
251
+ "top_pad",
252
+ "in_card",
253
+ ];
254
+ const patch = Object.fromEntries(
255
+ Object.entries(params).filter(([k]) => allowed.includes(k))
256
+ );
165
257
  const plugin = await findPlugin();
166
258
  if (plugin) {
167
- const allowed = [
168
- "mode",
169
- "menu_style",
170
- "colorscheme",
171
- "fixed_top",
172
- "fluid",
173
- "top_pad",
174
- "in_card",
175
- ];
176
- const patch = Object.fromEntries(
177
- Object.entries(params).filter(([k]) => allowed.includes(k))
178
- );
179
259
  await savePluginConfig(plugin, patch);
180
260
  }
181
- return { success: true };
261
+ const result = { success: true };
262
+ if (params.menu_style) {
263
+ const mergedConfig = { ...(plugin?.configuration || {}), ...patch };
264
+ result.page_structure = renderStructureSkeleton(mergedConfig);
265
+ }
266
+ return result;
182
267
  },
183
- renderToolResponse: async () =>
184
- div({ class: "text-success" }, "Layout configuration updated."),
268
+ renderToolResponse: async ({ page_structure }) =>
269
+ div(
270
+ { class: "text-success" },
271
+ "Layout configuration updated.",
272
+ page_structure
273
+ ? pre({ class: "mt-2 small" }, code(page_structure))
274
+ : ""
275
+ ),
185
276
  function: {
186
277
  name: "set_layout_config",
187
278
  description:
188
- "Set structural layout configuration for the Saltcorn UI. Only pass the parameters you want to change.",
279
+ "Set structural layout configuration for the Saltcorn UI. Only pass the parameters you want to change. When menu_style is set, the tool response includes a page_structure field containing rendered HTML of the new layout — use it to understand the correct selectors before writing CSS.",
189
280
  parameters: {
190
281
  type: "object",
191
282
  properties: {
package/index.js CHANGED
@@ -928,6 +928,65 @@ document.addEventListener('DOMContentLoaded', () => {
928
928
  ],
929
929
  });
930
930
 
931
+ const FAKE_MENU = [
932
+ {
933
+ section: "Main",
934
+ items: [
935
+ { label: "Tables", link: "/table" },
936
+ { label: "Views", link: "/viewedit" },
937
+ { label: "Pages", link: "/pageedit" },
938
+ {
939
+ label: "Settings",
940
+ subitems: [
941
+ { label: "About application", link: "/admin" },
942
+ { label: "Modules", link: "/plugins" },
943
+ { label: "Users and security", link: "/useradmin" },
944
+ ],
945
+ },
946
+ {
947
+ label: "User",
948
+ subitems: [
949
+ { label: "User settings", link: "/auth/settings" },
950
+ { label: "Logout", link: "/auth/logout" },
951
+ ],
952
+ },
953
+ ],
954
+ },
955
+ ];
956
+
957
+ const FAKE_LIST_BODY =
958
+ '<section class="page-section pt-2"><div class="container">' +
959
+ '<div class="table-responsive"><table class="table table-sm table-valign-middle">' +
960
+ "<thead><tr>" +
961
+ '<th><span class="link-style">Email</span></th>' +
962
+ '<th class="text-align-right"><span class="link-style">Role</span></th>' +
963
+ "</tr></thead>" +
964
+ '<tbody><tr data-row-id="1"><td>admin@foo.com</td><td class="text-align-right">1</td></tr></tbody>' +
965
+ "</table></div>" +
966
+ "</div></section>";
967
+
968
+ const renderStructureSkeleton = (config) =>
969
+ menuWrap({
970
+ brand: { name: "Brand" },
971
+ menu: FAKE_MENU,
972
+ config,
973
+ currentUrl: "/",
974
+ originalUrl: "/",
975
+ body: '<section class="page-section pt-2"><div class="container"><p><!-- page content --></p></div></section>',
976
+ req: null,
977
+ });
978
+
979
+ const renderPageSkeleton = (config) =>
980
+ menuWrap({
981
+ brand: { name: "Brand" },
982
+ menu: FAKE_MENU,
983
+ config,
984
+ currentUrl: "/",
985
+ originalUrl: "/",
986
+ body: FAKE_LIST_BODY,
987
+ req: null,
988
+ });
989
+
931
990
  module.exports = {
932
991
  sc_plugin_api_version: 1,
933
992
  plugin_name: "bootstrap-prompt-theme",
@@ -980,4 +1039,6 @@ module.exports = {
980
1039
  }
981
1040
  },
982
1041
  ready_for_mobile: true,
1042
+ renderStructureSkeleton,
1043
+ renderPageSkeleton,
983
1044
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/bootstrap-prompt-theme",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Bootstrap 5 layout plugin with LLM-generated CSS overlay",
5
5
  "main": "index.js",
6
6
  "scripts": {