@saltcorn/copilot 0.6.1 → 0.6.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.
- package/common.js +70 -20
- package/package.json +3 -2
- package/page-gen-action.js +120 -17
package/common.js
CHANGED
|
@@ -7,6 +7,7 @@ const Form = require("@saltcorn/data/models/form");
|
|
|
7
7
|
const View = require("@saltcorn/data/models/view");
|
|
8
8
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
9
9
|
const { getState } = require("@saltcorn/data/db/state");
|
|
10
|
+
const voidHtmlTags = new Set(require("html-tags/void"));
|
|
10
11
|
|
|
11
12
|
const parseCSS = require("style-to-object").default;
|
|
12
13
|
const MarkdownIt = require("markdown-it"),
|
|
@@ -232,25 +233,33 @@ function walk_response(segment) {
|
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
function parseHTML(str) {
|
|
236
|
+
function parseHTML(str, processAll) {
|
|
236
237
|
const strHtml = str.includes("```html")
|
|
237
238
|
? str.split("```html")[1].split("```")[0]
|
|
238
239
|
: str;
|
|
239
|
-
const body =
|
|
240
|
+
const body = processAll
|
|
241
|
+
? HTMLParser.parse(strHtml)
|
|
242
|
+
: HTMLParser.parse(strHtml).querySelector("body");
|
|
243
|
+
const btnSizeClasses = new Set(["btn-sm", "btn-xs", "btn-lg"]);
|
|
240
244
|
|
|
241
245
|
const go = (node) => {
|
|
246
|
+
//console.log("go node", node.toString());
|
|
247
|
+
|
|
242
248
|
if (node.constructor.name === "HTMLElement") {
|
|
243
249
|
switch (node.rawTagName) {
|
|
244
250
|
case "body":
|
|
245
251
|
return { above: node.childNodes.map(go).filter(Boolean) };
|
|
246
|
-
case "
|
|
252
|
+
case "script":
|
|
253
|
+
return null;
|
|
254
|
+
case "style":
|
|
247
255
|
return {
|
|
248
256
|
type: "blank",
|
|
249
|
-
|
|
250
|
-
|
|
257
|
+
isHTML: true,
|
|
258
|
+
contents: `<style>${node.childNodes
|
|
259
|
+
.map((n) => n.toString())
|
|
260
|
+
.join("")}</style>`,
|
|
261
|
+
text_strings: [node.childNodes.map((n) => n.toString()).join("")],
|
|
251
262
|
};
|
|
252
|
-
case "script":
|
|
253
|
-
return null;
|
|
254
263
|
case "a":
|
|
255
264
|
return {
|
|
256
265
|
type: "link",
|
|
@@ -261,15 +270,40 @@ function parseHTML(str) {
|
|
|
261
270
|
};
|
|
262
271
|
case "img":
|
|
263
272
|
return {
|
|
264
|
-
|
|
273
|
+
alt: node.getAttribute("alt") || "",
|
|
274
|
+
url: node.getAttribute("src") || "",
|
|
275
|
+
type: "image",
|
|
276
|
+
block: false,
|
|
265
277
|
style: {
|
|
266
|
-
"
|
|
267
|
-
"border-style": "solid",
|
|
268
|
-
"border-width": "2px",
|
|
278
|
+
"object-fit": "none",
|
|
269
279
|
},
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
srctype: "URL",
|
|
281
|
+
isFormula: {},
|
|
282
|
+
customClass: (node.classList.value || []).join(" "),
|
|
283
|
+
};
|
|
284
|
+
case "button":
|
|
285
|
+
return {
|
|
286
|
+
type: "action",
|
|
287
|
+
block: false,
|
|
288
|
+
rndid: Math.floor(Math.random() * 16777215).toString(16),
|
|
289
|
+
nsteps: 1,
|
|
290
|
+
confirm: false,
|
|
291
|
+
minRole: 100,
|
|
292
|
+
spinner: true,
|
|
293
|
+
isFormula: {},
|
|
294
|
+
action_icon: "",
|
|
295
|
+
action_name: "run_js_code",
|
|
296
|
+
action_label: node.childNodes.map((n) => n.toString()).join(""),
|
|
297
|
+
action_style:
|
|
298
|
+
(node.classList?.value || []).find(
|
|
299
|
+
(c) => c.startsWith("btn-") && !btnSizeClasses.has(c)
|
|
300
|
+
) || "btn-primary",
|
|
301
|
+
action_size: (node.classList?.value || []).find((c) =>
|
|
302
|
+
btnSizeClasses.has(c)
|
|
303
|
+
),
|
|
304
|
+
configuration: {
|
|
305
|
+
run_where: "Server",
|
|
306
|
+
code: "return {notify: 'Press button'}",
|
|
273
307
|
},
|
|
274
308
|
};
|
|
275
309
|
|
|
@@ -286,12 +320,28 @@ function parseHTML(str) {
|
|
|
286
320
|
textStyle: [node.rawTagName],
|
|
287
321
|
};
|
|
288
322
|
default:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
323
|
+
const containerContents = !node.childNodes.length
|
|
324
|
+
? ""
|
|
325
|
+
: node.childNodes.length === 1
|
|
326
|
+
? go(node.childNodes[0]) || ""
|
|
327
|
+
: { above: node.childNodes.map(go).filter(Boolean) };
|
|
328
|
+
if (voidHtmlTags.has(node.rawTagName))
|
|
329
|
+
return {
|
|
330
|
+
type: "blank",
|
|
331
|
+
isHTML: true,
|
|
332
|
+
contents: node.toString(),
|
|
333
|
+
text_strings: [],
|
|
334
|
+
};
|
|
335
|
+
else
|
|
336
|
+
return {
|
|
337
|
+
type: "container",
|
|
338
|
+
...(node.rawTagName && node.rawTagName !== "div"
|
|
339
|
+
? { htmlElement: node.rawTagName }
|
|
340
|
+
: {}),
|
|
341
|
+
...(node.id ? { customId: node.id } : {}),
|
|
342
|
+
customClass: (node.classList.value || []).join(" "),
|
|
343
|
+
contents: containerContents,
|
|
344
|
+
};
|
|
295
345
|
}
|
|
296
346
|
} else if (node.constructor.name === "TextNode") {
|
|
297
347
|
if (!node._rawText || !node._rawText.trim()) return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/copilot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "AI assistant for building Saltcorn applications",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"node-sql-parser": "4.15.0",
|
|
10
10
|
"markdown-it": "14.1.0",
|
|
11
11
|
"style-to-object": "1.0.8",
|
|
12
|
-
"node-html-parser": "7.0.1"
|
|
12
|
+
"node-html-parser": "7.0.1",
|
|
13
|
+
"html-tags": "3.3.1"
|
|
13
14
|
},
|
|
14
15
|
"author": "Tom Nielsen",
|
|
15
16
|
"license": "MIT",
|
package/page-gen-action.js
CHANGED
|
@@ -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",
|
|
@@ -42,6 +43,12 @@ module.exports = {
|
|
|
42
43
|
class: "validate-identifier",
|
|
43
44
|
type: "String",
|
|
44
45
|
},
|
|
46
|
+
{
|
|
47
|
+
name: "convert_to_saltcorn",
|
|
48
|
+
label: "Editable format",
|
|
49
|
+
sublabel: "Convert to Saltcorn editable pages",
|
|
50
|
+
type: "Bool",
|
|
51
|
+
},
|
|
45
52
|
// ...override_fields,
|
|
46
53
|
{
|
|
47
54
|
name: "model",
|
|
@@ -95,6 +102,7 @@ module.exports = {
|
|
|
95
102
|
answer_field,
|
|
96
103
|
image_prompt,
|
|
97
104
|
chat_history_field,
|
|
105
|
+
convert_to_saltcorn,
|
|
98
106
|
model,
|
|
99
107
|
},
|
|
100
108
|
}) => {
|
|
@@ -135,7 +143,7 @@ module.exports = {
|
|
|
135
143
|
for (const image of Array.isArray(from_ctx) ? from_ctx : [from_ctx]) {
|
|
136
144
|
const file = await File.findOne({ name: image });
|
|
137
145
|
const imageurl = await file.get_contents("base64");
|
|
138
|
-
|
|
146
|
+
|
|
139
147
|
chat.push({
|
|
140
148
|
role: "user",
|
|
141
149
|
content: [
|
|
@@ -154,7 +162,17 @@ module.exports = {
|
|
|
154
162
|
});
|
|
155
163
|
const initial_info = initial_ans.tool_calls[0].input;
|
|
156
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
|
|
157
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>`;
|
|
158
176
|
const page_html = await getState().functions.llm_generate.run(
|
|
159
177
|
`${prompt}.
|
|
160
178
|
|
|
@@ -162,13 +180,8 @@ module.exports = {
|
|
|
162
180
|
Further page description: ${initial_info.description}.
|
|
163
181
|
|
|
164
182
|
Generate the HTML for the web page using the Bootstrap 5 CSS framework.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
|
168
|
-
|
|
169
|
-
and
|
|
170
|
-
|
|
171
|
-
<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}
|
|
172
185
|
|
|
173
186
|
Just generate HTML code, do not wrap in markdown code tags`,
|
|
174
187
|
{
|
|
@@ -188,14 +201,35 @@ module.exports = {
|
|
|
188
201
|
|
|
189
202
|
const use_page_name = page_name ? interpolate(page_name, row, user) : "";
|
|
190
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
|
+
}
|
|
191
232
|
//save to a file
|
|
192
|
-
const file = await File.from_contents(
|
|
193
|
-
`${use_page_name}.html`,
|
|
194
|
-
"text/html",
|
|
195
|
-
page_html,
|
|
196
|
-
user.id,
|
|
197
|
-
100
|
|
198
|
-
);
|
|
199
233
|
|
|
200
234
|
//create page
|
|
201
235
|
await Page.create({
|
|
@@ -203,12 +237,81 @@ module.exports = {
|
|
|
203
237
|
title: initial_info.title,
|
|
204
238
|
description: initial_info.description,
|
|
205
239
|
min_role: 100,
|
|
206
|
-
layout
|
|
240
|
+
layout,
|
|
207
241
|
});
|
|
208
|
-
getState().refresh_pages();
|
|
242
|
+
setTimeout(() => getState().refresh_pages(), 200);
|
|
209
243
|
}
|
|
210
244
|
const upd = answer_field ? { [answer_field]: page_html } : {};
|
|
211
245
|
if (mode === "workflow") return upd;
|
|
212
246
|
else if (answer_field) await table.updateRow(upd, row[table.pk_name]);
|
|
213
247
|
},
|
|
214
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
|
+
`;
|