@saltcorn/copilot 0.6.0 → 0.6.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.
package/common.js CHANGED
@@ -232,25 +232,40 @@ function walk_response(segment) {
232
232
  }
233
233
  }
234
234
 
235
- function parseHTML(str) {
235
+ function parseHTML(str, processAll) {
236
236
  const strHtml = str.includes("```html")
237
237
  ? str.split("```html")[1].split("```")[0]
238
238
  : str;
239
- const body = HTMLParser.parse(strHtml).querySelector("body");
239
+ const body = processAll
240
+ ? HTMLParser.parse(strHtml)
241
+ : HTMLParser.parse(strHtml).querySelector("body");
242
+ const btnSizeClasses = new Set(["btn-sm", "btn-xs", "btn-lg"]);
240
243
 
241
244
  const go = (node) => {
245
+ //console.log("go node", node.toString());
246
+
242
247
  if (node.constructor.name === "HTMLElement") {
243
248
  switch (node.rawTagName) {
244
249
  case "body":
245
250
  return { above: node.childNodes.map(go).filter(Boolean) };
246
- case "p":
251
+ case "script":
252
+ return null;
253
+ case "style":
247
254
  return {
248
255
  type: "blank",
249
- contents: node.childNodes.map((n) => n.toString()).join(""),
250
- customClass: (node.classList.value || []).join(" "),
256
+ isHTML: true,
257
+ contents: `<style>${node.childNodes
258
+ .map((n) => n.toString())
259
+ .join("")}</style>`,
260
+ text_strings: [node.childNodes.map((n) => n.toString()).join("")],
261
+ };
262
+ case "input":
263
+ return {
264
+ type: "blank",
265
+ isHTML: true,
266
+ contents: node.toString(),
267
+ text_strings: [],
251
268
  };
252
- case "script":
253
- return null;
254
269
  case "a":
255
270
  return {
256
271
  type: "link",
@@ -261,15 +276,40 @@ function parseHTML(str) {
261
276
  };
262
277
  case "img":
263
278
  return {
264
- type: "container",
279
+ alt: node.getAttribute("alt") || "",
280
+ url: node.getAttribute("src") || "",
281
+ type: "image",
282
+ block: false,
265
283
  style: {
266
- "border-color": "#808080",
267
- "border-style": "solid",
268
- "border-width": "2px",
284
+ "object-fit": "none",
269
285
  },
270
- contents: {
271
- type: "blank",
272
- contents: "Image: " + node.getAttribute("alt"),
286
+ srctype: "URL",
287
+ isFormula: {},
288
+ customClass: (node.classList.value || []).join(" "),
289
+ };
290
+ case "button":
291
+ return {
292
+ type: "action",
293
+ block: false,
294
+ rndid: Math.floor(Math.random() * 16777215).toString(16),
295
+ nsteps: 1,
296
+ confirm: false,
297
+ minRole: 100,
298
+ spinner: true,
299
+ isFormula: {},
300
+ action_icon: "",
301
+ action_name: "run_js_code",
302
+ action_label: node.childNodes.map((n) => n.toString()).join(""),
303
+ action_style:
304
+ (node.classList?.value || []).find(
305
+ (c) => c.startsWith("btn-") && !btnSizeClasses.has(c)
306
+ ) || "btn-primary",
307
+ action_size: (node.classList?.value || []).find((c) =>
308
+ btnSizeClasses.has(c)
309
+ ),
310
+ configuration: {
311
+ run_where: "Server",
312
+ code: "return {notify: 'Press button'}",
273
313
  },
274
314
  };
275
315
 
@@ -286,11 +326,19 @@ function parseHTML(str) {
286
326
  textStyle: [node.rawTagName],
287
327
  };
288
328
  default:
329
+ const containerContents = !node.childNodes.length
330
+ ? ""
331
+ : node.childNodes.length === 1
332
+ ? go(node.childNodes[0]) || ""
333
+ : { above: node.childNodes.map(go).filter(Boolean) };
289
334
  return {
290
335
  type: "container",
291
- htmlElement: node.rawTagName,
336
+ ...(node.rawTagName && node.rawTagName !== "div"
337
+ ? { htmlElement: node.rawTagName }
338
+ : {}),
339
+ ...(node.id ? { customId: node.id } : {}),
292
340
  customClass: (node.classList.value || []).join(" "),
293
- contents: node.childNodes.map(go).filter(Boolean),
341
+ contents: containerContents,
294
342
  };
295
343
  }
296
344
  } else if (node.constructor.name === "TextNode") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/copilot",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "AI assistant for building Saltcorn applications",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -5,6 +5,7 @@ const File = require("@saltcorn/data/models/file");
5
5
  const Page = require("@saltcorn/data/models/page");
6
6
 
7
7
  const GeneratePage = require("./actions/generate-page");
8
+ const { parseHTML } = require("./common");
8
9
 
9
10
  module.exports = {
10
11
  description: "Generate page with AI copilot",
@@ -27,12 +28,26 @@ module.exports = {
27
28
  fieldview: "textarea",
28
29
  required: true,
29
30
  },
31
+ {
32
+ name: "image_prompt",
33
+ label: "Prompt image files",
34
+ sublabel:
35
+ "Optional. An expression, based on the context, for file path or array of file paths for prompting",
36
+ class: "validate-expression",
37
+ type: "String",
38
+ },
30
39
  {
31
40
  name: "answer_field",
32
41
  label: "Answer variable",
33
42
  sublabel: "Optional. Set the generated HTML to this context variable",
43
+ class: "validate-identifier",
34
44
  type: "String",
35
- required: true,
45
+ },
46
+ {
47
+ name: "convert_to_saltcorn",
48
+ label: "Editable format",
49
+ sublabel: "Convert to Saltcorn editable pages",
50
+ type: "Bool",
36
51
  },
37
52
  // ...override_fields,
38
53
  {
@@ -85,7 +100,9 @@ module.exports = {
85
100
  prompt_formula,
86
101
  prompt_template,
87
102
  answer_field,
103
+ image_prompt,
88
104
  chat_history_field,
105
+ convert_to_saltcorn,
89
106
  model,
90
107
  },
91
108
  }) => {
@@ -96,7 +113,7 @@ module.exports = {
96
113
  prompt_formula,
97
114
  row,
98
115
  user,
99
- "llm_generate prompt formula"
116
+ "copilot_generate_page prompt formula"
100
117
  );
101
118
  else prompt = row[prompt_field];
102
119
  const opts = {};
@@ -113,14 +130,49 @@ module.exports = {
113
130
  },
114
131
  });
115
132
  const { llm_generate } = getState().functions;
133
+ let chat;
134
+ if (image_prompt) {
135
+ const from_ctx = eval_expression(
136
+ image_prompt,
137
+ row,
138
+ user,
139
+ "copilot_generate_page image prompt"
140
+ );
141
+
142
+ chat = [];
143
+ for (const image of Array.isArray(from_ctx) ? from_ctx : [from_ctx]) {
144
+ const file = await File.findOne({ name: image });
145
+ const imageurl = await file.get_contents("base64");
116
146
 
147
+ chat.push({
148
+ role: "user",
149
+ content: [
150
+ {
151
+ type: "image",
152
+ image: `data:${file.mimetype};base64,${imageurl}`,
153
+ },
154
+ ],
155
+ });
156
+ }
157
+ }
117
158
  const initial_ans = await llm_generate.run(prompt, {
118
159
  tools,
160
+ chat,
119
161
  systemPrompt,
120
162
  });
121
163
  const initial_info = initial_ans.tool_calls[0].input;
122
164
  const full = await GeneratePage.follow_on_generate(initial_info);
165
+ const prompt_part_2 = convert_to_saltcorn
166
+ ? `Only generate the inner part of the body.
167
+ Do not include the top menu. Generate the HTML that comes below the navbar menu.
168
+ if you want to change the overall styling of the page, include a <style> element where you can change styles with CSS rules or CSS variables.`
169
+ : `If you need to include the standard bootstrap CSS and javascript files, they are available as:
170
+
171
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
172
+
173
+ and
123
174
 
175
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>`;
124
176
  const page_html = await getState().functions.llm_generate.run(
125
177
  `${prompt}.
126
178
 
@@ -128,17 +180,13 @@ module.exports = {
128
180
  Further page description: ${initial_info.description}.
129
181
 
130
182
  Generate the HTML for the web page using the Bootstrap 5 CSS framework.
131
- If you need to include the standard bootstrap CSS and javascript files, they are available as:
132
-
133
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
134
-
135
- and
136
-
137
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
183
+
184
+ ${prompt_part_2}
138
185
 
139
186
  Just generate HTML code, do not wrap in markdown code tags`,
140
187
  {
141
188
  debugResult: true,
189
+ chat,
142
190
  response_format: full.response_schema
143
191
  ? {
144
192
  type: "json_schema",
@@ -153,14 +201,35 @@ module.exports = {
153
201
 
154
202
  const use_page_name = page_name ? interpolate(page_name, row, user) : "";
155
203
  if (use_page_name) {
204
+ let layout;
205
+ if (convert_to_saltcorn) {
206
+ layout = parseHTML(page_html, true);
207
+ //console.log("got layout", JSON.stringify(layout, null, 2));
208
+ const file = await File.from_contents(
209
+ `${use_page_name}.html`,
210
+ "text/html",
211
+ wrapExample(page_html),
212
+ user.id,
213
+ 100
214
+ );
215
+ await Page.create({
216
+ name: use_page_name + "_html",
217
+ title: initial_info.title,
218
+ description: initial_info.description,
219
+ min_role: 100,
220
+ layout: { html_file: file.path_to_serve },
221
+ });
222
+ } else {
223
+ const file = await File.from_contents(
224
+ `${use_page_name}.html`,
225
+ "text/html",
226
+ page_html,
227
+ user.id,
228
+ 100
229
+ );
230
+ layout = { html_file: file.path_to_serve };
231
+ }
156
232
  //save to a file
157
- const file = await File.from_contents(
158
- `${use_page_name}.html`,
159
- "text/html",
160
- page_html,
161
- user.id,
162
- 100
163
- );
164
233
 
165
234
  //create page
166
235
  await Page.create({
@@ -168,12 +237,81 @@ module.exports = {
168
237
  title: initial_info.title,
169
238
  description: initial_info.description,
170
239
  min_role: 100,
171
- layout: { html_file: file.path_to_serve },
240
+ layout,
172
241
  });
173
- getState().refresh_pages()
242
+ setTimeout(() => getState().refresh_pages(), 200);
174
243
  }
175
244
  const upd = answer_field ? { [answer_field]: page_html } : {};
176
245
  if (mode === "workflow") return upd;
177
246
  else if (answer_field) await table.updateRow(upd, row[table.pk_name]);
178
247
  },
179
248
  };
249
+
250
+ const wrapExample = (inner) => `
251
+ <!DOCTYPE html>
252
+ <html lang="en">
253
+ <head>
254
+ <meta charset="utf-8" />
255
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
256
+ <title>Artisan Goat Cheese.</title>
257
+ <meta name="description" content="Handcrafted, small-batch goat cheese made from ethically sourced milk. Farm-to-table flavors with aged, tangy, and creamy varieties. Available online and at select markets, with subscription options and artisan pairings." />
258
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
259
+ <style>
260
+ :root {
261
+ --brand: #7a7a4a;
262
+ }
263
+ body {
264
+ background-color: #fff;
265
+ }
266
+ /* Hero styling */
267
+ #home {
268
+ padding-top: 2rem;
269
+ padding-bottom: 2rem;
270
+ }
271
+ .hero {
272
+ background: linear-gradient(135deg, #f7f4ef 0%, #ffffff 60%);
273
+ border-bottom: 1px solid #eee;
274
+ }
275
+ .hero-img {
276
+ max-height: 420px;
277
+ object-fit: cover;
278
+ width: 100%;
279
+ border-radius: 0.5rem;
280
+ border: 1px solid #eee;
281
+ }
282
+ /* Card image sizing for consistency */
283
+ .card-img-top {
284
+ height: 180px;
285
+ object-fit: cover;
286
+ }
287
+ </style>
288
+ </head>
289
+ <body>
290
+
291
+ <!-- Navbar -->
292
+ <nav class="navbar navbar-expand-lg navbar-light bg-white sticky-top shadow-sm" aria-label="Main navigation">
293
+ <div class="container">
294
+ <a class="navbar-brand fw-semibold" href="#home">Artisan Goat Cheese</a>
295
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu" aria-controls="navMenu" aria-expanded="false" aria-label="Toggle navigation">
296
+ <span class="navbar-toggler-icon"></span>
297
+ </button>
298
+ <div class="collapse navbar-collapse" id="navMenu">
299
+ <ul class="navbar-nav ms-auto mb-2 mb-lg-0">
300
+ <li class="nav-item"><a class="nav-link active" aria-current="page" href="#home">Home</a></li>
301
+ <li class="nav-item"><a class="nav-link" href="#products">Cheeses</a></li>
302
+ <li class="nav-item"><a class="nav-link" href="#subscription">Subscriptions</a></li>
303
+ <li class="nav-item"><a class="nav-link" href="#markets">Markets</a></li>
304
+ <li class="nav-item"><a class="nav-link" href="#pairings">Pairings</a></li>
305
+ <li class="nav-item"><a class="nav-link" href="#about">About</a></li>
306
+ </ul>
307
+ </div>
308
+ </div>
309
+ </nav>
310
+
311
+ ${inner}
312
+
313
+ <!-- Bootstrap JS -->
314
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
315
+ </body>
316
+ </html>
317
+ `;