@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 +64 -16
- package/package.json +1 -1
- package/page-gen-action.js +156 -18
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 =
|
|
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 "
|
|
251
|
+
case "script":
|
|
252
|
+
return null;
|
|
253
|
+
case "style":
|
|
247
254
|
return {
|
|
248
255
|
type: "blank",
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
279
|
+
alt: node.getAttribute("alt") || "",
|
|
280
|
+
url: node.getAttribute("src") || "",
|
|
281
|
+
type: "image",
|
|
282
|
+
block: false,
|
|
265
283
|
style: {
|
|
266
|
-
"
|
|
267
|
-
"border-style": "solid",
|
|
268
|
-
"border-width": "2px",
|
|
284
|
+
"object-fit": "none",
|
|
269
285
|
},
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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:
|
|
341
|
+
contents: containerContents,
|
|
294
342
|
};
|
|
295
343
|
}
|
|
296
344
|
} else if (node.constructor.name === "TextNode") {
|
package/package.json
CHANGED
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",
|
|
@@ -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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
+
`;
|