@saltcorn/copilot 0.4.4 → 0.5.0
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/actions/generate-page.js +450 -0
- package/actions/generate-tables.js +3 -3
- package/actions/generate-view.js +299 -0
- package/actions/generate-workflow.js +10 -3
- package/chat-copilot.js +95 -9
- package/common.js +213 -1
- package/package.json +4 -2
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
2
|
+
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
3
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
4
|
+
const Page = require("@saltcorn/data/models/page");
|
|
5
|
+
const Table = require("@saltcorn/data/models/table");
|
|
6
|
+
const User = require("@saltcorn/data/models/user");
|
|
7
|
+
const Field = require("@saltcorn/data/models/field");
|
|
8
|
+
const { apply, removeAllWhiteSpace } = require("@saltcorn/data/utils");
|
|
9
|
+
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
10
|
+
const { a, pre, script, div, code } = require("@saltcorn/markup/tags");
|
|
11
|
+
const {
|
|
12
|
+
fieldProperties,
|
|
13
|
+
getPromptFromTemplate,
|
|
14
|
+
splitContainerStyle,
|
|
15
|
+
containerHandledStyles,
|
|
16
|
+
parseHTML,
|
|
17
|
+
} = require("../common");
|
|
18
|
+
const MarkdownIt = require("markdown-it"),
|
|
19
|
+
md = new MarkdownIt();
|
|
20
|
+
|
|
21
|
+
class GeneratePage {
|
|
22
|
+
static title = "Generate Page";
|
|
23
|
+
static function_name = "generate_page";
|
|
24
|
+
static description = "Generate Page";
|
|
25
|
+
|
|
26
|
+
static async json_schema() {
|
|
27
|
+
const allPageNames = (await Page.find({})).map((p) => p.page);
|
|
28
|
+
let namedescription = `The name of the page, this should be a short name which is part of the url. `;
|
|
29
|
+
if (allPageNames.length) {
|
|
30
|
+
namedescription += `These are the names of the exising pages: ${allPageNames.join(
|
|
31
|
+
", "
|
|
32
|
+
)}. Do not pick a name that is identical but follow the same naming convention.`;
|
|
33
|
+
}
|
|
34
|
+
const roles = await User.get_roles();
|
|
35
|
+
return {
|
|
36
|
+
type: "object",
|
|
37
|
+
required: ["name", "title", "min_role", "page_type"],
|
|
38
|
+
properties: {
|
|
39
|
+
name: {
|
|
40
|
+
description: namedescription,
|
|
41
|
+
type: "string",
|
|
42
|
+
},
|
|
43
|
+
title: {
|
|
44
|
+
description: "Page title, this is in the <title> tag.",
|
|
45
|
+
type: "string",
|
|
46
|
+
},
|
|
47
|
+
description: {
|
|
48
|
+
description:
|
|
49
|
+
"A longer description that is not visible but appears in the page header and is indexed by search engines",
|
|
50
|
+
type: "string",
|
|
51
|
+
},
|
|
52
|
+
min_role: {
|
|
53
|
+
description:
|
|
54
|
+
"The minimum role needed to access the page. For pages accessible only by admin, use 'admin', pages with min_role 'public' is publicly accessible and also available to all users",
|
|
55
|
+
type: "string",
|
|
56
|
+
enum: roles.map((r) => r.role),
|
|
57
|
+
},
|
|
58
|
+
page_type: {
|
|
59
|
+
description:
|
|
60
|
+
"The type of page to generate: a Marketing page if for promotional purposes, such as a landing page or a brouchure, with an appealing design. An Application page is simpler and an integrated part of the application",
|
|
61
|
+
type: "string",
|
|
62
|
+
enum: ["Marketing page", "Application page"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
static async system_prompt() {
|
|
68
|
+
return `Use the generate_page to generate a page.`;
|
|
69
|
+
}
|
|
70
|
+
static async follow_on_generate({ name, page_type }) {
|
|
71
|
+
if (page_type === "Marketing page") {
|
|
72
|
+
return {
|
|
73
|
+
prompt:
|
|
74
|
+
"Generate the HTML for the web page using the Bootstrap 5 CSS framework.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const prompt = `Now generate the contents of the ${name} page`;
|
|
78
|
+
const response_schema = {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
element: {
|
|
82
|
+
anyOf: [
|
|
83
|
+
{
|
|
84
|
+
type: "object",
|
|
85
|
+
description: "Position items next to each other in grid columns",
|
|
86
|
+
//required: ["name", "title", "min_role"],
|
|
87
|
+
properties: {
|
|
88
|
+
besides: {
|
|
89
|
+
type: "array",
|
|
90
|
+
items: {
|
|
91
|
+
type: "object",
|
|
92
|
+
$ref: "#",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
widths: {
|
|
96
|
+
type: "array",
|
|
97
|
+
items: {
|
|
98
|
+
type: "integer",
|
|
99
|
+
description:
|
|
100
|
+
"The width of each column 1-12. The sum of all columns must equal 12",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: "object",
|
|
107
|
+
//required: ["name", "title", "min_role"],
|
|
108
|
+
description: "Position items vertically, each above the next",
|
|
109
|
+
properties: {
|
|
110
|
+
above: {
|
|
111
|
+
type: "array",
|
|
112
|
+
items: {
|
|
113
|
+
type: "object",
|
|
114
|
+
$ref: "#",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: "object",
|
|
121
|
+
required: ["type", "isHTML", "contents"],
|
|
122
|
+
description: "An element containing HTML",
|
|
123
|
+
properties: {
|
|
124
|
+
type: { const: "blank" },
|
|
125
|
+
isHTML: { const: true },
|
|
126
|
+
contents: {
|
|
127
|
+
type: "string",
|
|
128
|
+
description: "The HTML contents of this element",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: "object",
|
|
134
|
+
required: ["type", "contents"],
|
|
135
|
+
description: "An element containing text",
|
|
136
|
+
properties: {
|
|
137
|
+
type: { const: "blank" },
|
|
138
|
+
contents: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "The plain text contents of this element",
|
|
141
|
+
},
|
|
142
|
+
style: {
|
|
143
|
+
type: "object",
|
|
144
|
+
description:
|
|
145
|
+
"Some CSS properties that can be applied to the text element",
|
|
146
|
+
properties: {
|
|
147
|
+
"font-size": {
|
|
148
|
+
type: "string",
|
|
149
|
+
description:
|
|
150
|
+
"CSS size identifier, for example 12px or 2rem",
|
|
151
|
+
},
|
|
152
|
+
color: {
|
|
153
|
+
type: "string",
|
|
154
|
+
description:
|
|
155
|
+
"CSS color specifier, for example #15d48a or rgb(0, 255, 0)",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
textStyle: {
|
|
160
|
+
type: "array",
|
|
161
|
+
description: "The style to apply to the text",
|
|
162
|
+
items: {
|
|
163
|
+
type: "string",
|
|
164
|
+
description:
|
|
165
|
+
"h1-h6 to put in a header element. fst-italic for italic, text-muted for muted color, fw-bold for bold, text-underline for underline, small for smaller size, font-monospace for monospace font",
|
|
166
|
+
enum: [
|
|
167
|
+
"h1",
|
|
168
|
+
"h2",
|
|
169
|
+
"h3",
|
|
170
|
+
"h4",
|
|
171
|
+
"h5",
|
|
172
|
+
"h6",
|
|
173
|
+
"fst-italic",
|
|
174
|
+
"text-muted",
|
|
175
|
+
"fw-bold",
|
|
176
|
+
"text-underline",
|
|
177
|
+
"small",
|
|
178
|
+
"font-monospace",
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: "object",
|
|
186
|
+
required: ["type", "contents"],
|
|
187
|
+
description: "An container element that can set various styles",
|
|
188
|
+
properties: {
|
|
189
|
+
type: { const: "container" },
|
|
190
|
+
contents: {
|
|
191
|
+
type: "object",
|
|
192
|
+
$ref: "#",
|
|
193
|
+
},
|
|
194
|
+
style: {
|
|
195
|
+
type: "string",
|
|
196
|
+
description:
|
|
197
|
+
"CSS properties to set on the container formatted as the html style attribute, with no CSS selector and separated by semi-colons. Example: color: #00ff00; margin-top: 5px",
|
|
198
|
+
},
|
|
199
|
+
customClass: {
|
|
200
|
+
type: "string",
|
|
201
|
+
description:
|
|
202
|
+
"Custom class to set. You can use bootstrap 5 utility classes here as bootstrap 5 is loaded",
|
|
203
|
+
},
|
|
204
|
+
htmlElement: {
|
|
205
|
+
type: "string",
|
|
206
|
+
description: "The HTML element to use for the container",
|
|
207
|
+
enum: [
|
|
208
|
+
"div",
|
|
209
|
+
"span",
|
|
210
|
+
"article",
|
|
211
|
+
"section",
|
|
212
|
+
"header",
|
|
213
|
+
"nav",
|
|
214
|
+
"main",
|
|
215
|
+
"aside",
|
|
216
|
+
"footer",
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: "object",
|
|
223
|
+
description: "An image",
|
|
224
|
+
properties: {
|
|
225
|
+
type: { const: "image" },
|
|
226
|
+
description: {
|
|
227
|
+
type: "string",
|
|
228
|
+
description: "A description of the contents of the image",
|
|
229
|
+
},
|
|
230
|
+
width: {
|
|
231
|
+
type: "integer",
|
|
232
|
+
description: "The width of the image in px",
|
|
233
|
+
},
|
|
234
|
+
height: {
|
|
235
|
+
type: "integer",
|
|
236
|
+
description: "The height of the image in px",
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
return { response_schema, prompt };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static walk_response(segment) {
|
|
248
|
+
let go = GeneratePage.walk_response;
|
|
249
|
+
if (!segment) return segment;
|
|
250
|
+
if (typeof segment === "string") return segment;
|
|
251
|
+
if (segment.element) return go(segment.element);
|
|
252
|
+
if (Array.isArray(segment)) {
|
|
253
|
+
return segment.map(go);
|
|
254
|
+
}
|
|
255
|
+
if (typeof segment.contents === "string") {
|
|
256
|
+
return { ...segment, contents: md.render(segment.contents) };
|
|
257
|
+
}
|
|
258
|
+
if (segment.type === "image") {
|
|
259
|
+
return {
|
|
260
|
+
type: "container",
|
|
261
|
+
style: {
|
|
262
|
+
height: `${segment.height}px`,
|
|
263
|
+
width: `${segment.width}px`,
|
|
264
|
+
"border-style": "solid",
|
|
265
|
+
"border-color": "#808080",
|
|
266
|
+
"border-width": "3px",
|
|
267
|
+
vAlign: "middle",
|
|
268
|
+
hAlign: "center",
|
|
269
|
+
},
|
|
270
|
+
contents: segment.description,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (segment.type === "container") {
|
|
274
|
+
const { customStyle, style, display, overflow } = splitContainerStyle(
|
|
275
|
+
segment.style
|
|
276
|
+
);
|
|
277
|
+
return {
|
|
278
|
+
...segment,
|
|
279
|
+
customStyle,
|
|
280
|
+
display,
|
|
281
|
+
overflow,
|
|
282
|
+
style,
|
|
283
|
+
contents: go(segment.contents),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (segment.contents) {
|
|
287
|
+
return { ...segment, contents: go(segment.contents) };
|
|
288
|
+
}
|
|
289
|
+
if (segment.above) {
|
|
290
|
+
return { ...segment, above: go(segment.above) };
|
|
291
|
+
}
|
|
292
|
+
if (segment.besides) {
|
|
293
|
+
return { ...segment, besides: go(segment.besides) };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
static render_html(attrs, contents) {
|
|
298
|
+
if (attrs.page_type === "Marketing page") {
|
|
299
|
+
return (
|
|
300
|
+
pre(code(JSON.stringify(attrs, null, 2))) +
|
|
301
|
+
pre(code(escapeHtml(contents || "")))
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
pre(code(JSON.stringify(attrs, null, 2))) +
|
|
307
|
+
pre(
|
|
308
|
+
code(
|
|
309
|
+
escapeHtml(
|
|
310
|
+
JSON.stringify(
|
|
311
|
+
GeneratePage.walk_response(JSON.parse(contents)),
|
|
312
|
+
null,
|
|
313
|
+
2
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
static async execute(
|
|
321
|
+
{ name, title, description, min_role, page_type },
|
|
322
|
+
req,
|
|
323
|
+
contents
|
|
324
|
+
) {
|
|
325
|
+
console.log("execute", name, contents);
|
|
326
|
+
const roles = await User.get_roles();
|
|
327
|
+
const min_role_id = roles.find((r) => r.role === min_role).id;
|
|
328
|
+
let layout;
|
|
329
|
+
if (page_type === "Marketing page") {
|
|
330
|
+
layout = parseHTML(contents);
|
|
331
|
+
} else layout = GeneratePage.walk_response(JSON.parse(contents));
|
|
332
|
+
await Page.create({
|
|
333
|
+
name,
|
|
334
|
+
title,
|
|
335
|
+
description,
|
|
336
|
+
min_role: min_role_id,
|
|
337
|
+
layout,
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
postExec:
|
|
341
|
+
"Page created. " +
|
|
342
|
+
a(
|
|
343
|
+
{ target: "_blank", href: `/page/${name}`, class: "me-1" },
|
|
344
|
+
"Go to page"
|
|
345
|
+
) +
|
|
346
|
+
" | " +
|
|
347
|
+
a(
|
|
348
|
+
{ target: "_blank", href: `/pageedit/edit/${name}`, class: "ms-1" },
|
|
349
|
+
"Configure page"
|
|
350
|
+
),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function escapeHtml(unsafe) {
|
|
356
|
+
return unsafe
|
|
357
|
+
.replace(/&/g, "&")
|
|
358
|
+
.replace(/</g, "<")
|
|
359
|
+
.replace(/>/g, ">")
|
|
360
|
+
.replace(/"/g, """)
|
|
361
|
+
.replace(/'/g, "'");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
parseHTML(`<!DOCTYPE html>
|
|
365
|
+
<html lang="en">
|
|
366
|
+
<head>
|
|
367
|
+
<meta charset="UTF-8">
|
|
368
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
369
|
+
<title>Popcorn Beer - Unleash the Flavor Explosion!</title>
|
|
370
|
+
<link href="https://stackpath.bootstrapcdn.com/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
|
371
|
+
</head>
|
|
372
|
+
<body>
|
|
373
|
+
|
|
374
|
+
<header class="bg-warning text-dark py-5">
|
|
375
|
+
<div class="container text-center">
|
|
376
|
+
<h1 class="display-4">Popcorn Beer</h1>
|
|
377
|
+
<p class="lead">Unleash the Flavor Explosion!</p>
|
|
378
|
+
</div>
|
|
379
|
+
</header>
|
|
380
|
+
|
|
381
|
+
<section class="py-5">
|
|
382
|
+
<div class="container">
|
|
383
|
+
<div class="row align-items-center">
|
|
384
|
+
<div class="col-md-6">
|
|
385
|
+
<img src="your-image-url.jpg" alt="Popcorn Beer" class="img-fluid rounded">
|
|
386
|
+
</div>
|
|
387
|
+
<div class="col-md-6">
|
|
388
|
+
<h2>Discover the Unique Taste</h2>
|
|
389
|
+
<p>Introducing our popcorn-flavored beer, a delightful fusion of classic beer with a twist of popcorn essence. Experience the taste sensation that will keep you coming back for more.</p>
|
|
390
|
+
<a href="#purchase" class="btn btn-warning btn-lg mt-3">Buy Now</a>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</section>
|
|
395
|
+
|
|
396
|
+
<section class="bg-light py-5">
|
|
397
|
+
<div class="container">
|
|
398
|
+
<h2 class="text-center mb-4">Why Choose Popcorn Beer?</h2>
|
|
399
|
+
<div class="row">
|
|
400
|
+
<div class="col-lg-4">
|
|
401
|
+
<div class="card mb-4">
|
|
402
|
+
<div class="card-body text-center">
|
|
403
|
+
<h5 class="card-title">Unique Flavor</h5>
|
|
404
|
+
<p class="card-text">A perfect blend of beer and popcorn that tantalizes your taste buds.</p>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="col-lg-4">
|
|
409
|
+
<div class="card mb-4">
|
|
410
|
+
<div class="card-body text-center">
|
|
411
|
+
<h5 class="card-title">Premium Ingredients</h5>
|
|
412
|
+
<p class="card-text">Crafted with the finest ingredients for an unparalleled taste.</p>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
<div class="col-lg-4">
|
|
417
|
+
<div class="card mb-4">
|
|
418
|
+
<div class="card-body text-center">
|
|
419
|
+
<h5 class="card-title">Perfect for Any Occasion</h5>
|
|
420
|
+
<p class="card-text">Whether it's a movie night or a party, Popcorn Beer is your go-to drink.</p>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
</section>
|
|
427
|
+
|
|
428
|
+
<section id="purchase" class="py-5">
|
|
429
|
+
<div class="container text-center">
|
|
430
|
+
<h2>Get Your Popcorn Beer Today!</h2>
|
|
431
|
+
<p class="mb-4">Available for purchase online and in select stores near you.</p>
|
|
432
|
+
<a href="shop.html" class="btn btn-warning btn-lg">Shop Now</a>
|
|
433
|
+
</div>
|
|
434
|
+
</section>
|
|
435
|
+
|
|
436
|
+
<footer class="bg-dark text-white py-4">
|
|
437
|
+
<div class="container text-center">
|
|
438
|
+
<p>© 2023 Popcorn Beer. All rights reserved.</p>
|
|
439
|
+
<ul class="list-inline">
|
|
440
|
+
<li class="list-inline-item"><a href="#" class="text-white">Privacy Policy</a></li>
|
|
441
|
+
<li class="list-inline-item"><a href="#" class="text-white">Terms of Service</a></li>
|
|
442
|
+
<li class="list-inline-item"><a href="#" class="text-white">Contact Us</a></li>
|
|
443
|
+
</ul>
|
|
444
|
+
</div>
|
|
445
|
+
</footer>
|
|
446
|
+
|
|
447
|
+
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
|
|
448
|
+
</body>`);
|
|
449
|
+
|
|
450
|
+
module.exports = GeneratePage;
|
|
@@ -17,7 +17,7 @@ class GenerateTables {
|
|
|
17
17
|
const types = Object.values(getState().types);
|
|
18
18
|
const fieldTypeCfg = types.map((ty) => {
|
|
19
19
|
const properties = {
|
|
20
|
-
data_type: {
|
|
20
|
+
data_type: { type: "string", enum: [ty.name] },
|
|
21
21
|
};
|
|
22
22
|
const attrs = apply(ty.attributes, {}) || [];
|
|
23
23
|
if (Array.isArray(attrs))
|
|
@@ -40,7 +40,7 @@ class GenerateTables {
|
|
|
40
40
|
description:
|
|
41
41
|
"A foreign key to a different table. This will reference the primary key on another table.",
|
|
42
42
|
properties: {
|
|
43
|
-
data_type: {
|
|
43
|
+
data_type: { type: "string", enum: ["ForeignKey"] },
|
|
44
44
|
reference_table: {
|
|
45
45
|
type: "string",
|
|
46
46
|
description: "Name of the table being referenced",
|
|
@@ -52,7 +52,7 @@ class GenerateTables {
|
|
|
52
52
|
description:
|
|
53
53
|
"A reference (file path) to a file on disk. This can be used for example to hold images or documents",
|
|
54
54
|
properties: {
|
|
55
|
-
data_type: {
|
|
55
|
+
data_type: { type: "string", enum: ["File"] },
|
|
56
56
|
},
|
|
57
57
|
});
|
|
58
58
|
return {
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
2
|
+
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
3
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
4
|
+
const Page = require("@saltcorn/data/models/page");
|
|
5
|
+
const View = require("@saltcorn/data/models/view");
|
|
6
|
+
const Table = require("@saltcorn/data/models/table");
|
|
7
|
+
const User = require("@saltcorn/data/models/user");
|
|
8
|
+
const Field = require("@saltcorn/data/models/field");
|
|
9
|
+
const { apply, removeAllWhiteSpace } = require("@saltcorn/data/utils");
|
|
10
|
+
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
11
|
+
const { a, pre, script, div, code } = require("@saltcorn/markup/tags");
|
|
12
|
+
const {
|
|
13
|
+
fieldProperties,
|
|
14
|
+
getPromptFromTemplate,
|
|
15
|
+
splitContainerStyle,
|
|
16
|
+
containerHandledStyles,
|
|
17
|
+
walk_response,
|
|
18
|
+
} = require("../common");
|
|
19
|
+
|
|
20
|
+
const get_rndid = () => {
|
|
21
|
+
let characters = "0123456789abcdef";
|
|
22
|
+
let str = "";
|
|
23
|
+
for (let i = 0; i < 6; i++) {
|
|
24
|
+
str += characters[Math.floor(Math.random() * 16)];
|
|
25
|
+
}
|
|
26
|
+
return str;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const snakeToPascal = (str) => {
|
|
30
|
+
const snakeToCamel = (str) =>
|
|
31
|
+
str.replace(/([-_]\w)/g, (g) => g[1].toUpperCase());
|
|
32
|
+
let camelCase = snakeToCamel(str);
|
|
33
|
+
let pascalCase = camelCase[0].toUpperCase() + camelCase.substr(1);
|
|
34
|
+
return pascalCase;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const col2layoutSegment = (table, col) => {
|
|
38
|
+
switch (col.coltype) {
|
|
39
|
+
case "Field":
|
|
40
|
+
return {
|
|
41
|
+
type: "field",
|
|
42
|
+
field_name: col.fieldSpec.fieldname,
|
|
43
|
+
fieldview: col.fieldSpec.fieldview,
|
|
44
|
+
};
|
|
45
|
+
case "Action":
|
|
46
|
+
return {
|
|
47
|
+
type: "action",
|
|
48
|
+
action_name: col.action.action_name,
|
|
49
|
+
action_style: col.style,
|
|
50
|
+
action_label: col.label,
|
|
51
|
+
rndid: get_rndid(),
|
|
52
|
+
};
|
|
53
|
+
case "View link":
|
|
54
|
+
return {
|
|
55
|
+
type: "view_link",
|
|
56
|
+
view: col.view,
|
|
57
|
+
relation: `.${table.name}`,
|
|
58
|
+
link_style: col.style === "Link" ? "" : col.style,
|
|
59
|
+
view_label: col.label,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
default:
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
type: col.coltype.toLowerCase(),
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const segment2column = (seg) => {
|
|
71
|
+
return {
|
|
72
|
+
...seg,
|
|
73
|
+
type: snakeToPascal(seg.type),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
class GenerateView {
|
|
78
|
+
static title = "Generate View";
|
|
79
|
+
static function_name = "generate_view";
|
|
80
|
+
static description = "Generate view";
|
|
81
|
+
|
|
82
|
+
static async json_schema() {
|
|
83
|
+
const tables = await Table.find({});
|
|
84
|
+
//const viewtypes = getState().
|
|
85
|
+
const roles = await User.get_roles();
|
|
86
|
+
return {
|
|
87
|
+
type: "object",
|
|
88
|
+
required: ["name", "viewpattern", "table"],
|
|
89
|
+
properties: {
|
|
90
|
+
name: {
|
|
91
|
+
description: `The name of the view, this should be a short name which is part of the url. `,
|
|
92
|
+
type: "string",
|
|
93
|
+
},
|
|
94
|
+
viewpattern: {
|
|
95
|
+
description: `The type of view to generate. Show for a read-only view of a single table row,
|
|
96
|
+
List for a tabular grid view of many rows, Edit for forms for editing a single row, Feed to repeatedly show
|
|
97
|
+
another view (typically a Show view), Filter for selecting a subset of rows based on field values
|
|
98
|
+
to be shown in another view (typically List or Feed views) on the same page.`,
|
|
99
|
+
type: "string",
|
|
100
|
+
enum: ["List"], //, "Show", "Edit", "Filter", "Feed"],
|
|
101
|
+
},
|
|
102
|
+
table: {
|
|
103
|
+
description: "Which table is this a view on",
|
|
104
|
+
type: "string",
|
|
105
|
+
enum: tables.map((t) => t.name),
|
|
106
|
+
},
|
|
107
|
+
min_role: {
|
|
108
|
+
description:
|
|
109
|
+
"The minimum role needed to access the view. For vies accessible only by admin, use 'admin', pages with min_role 'public' is publicly accessible and also available to all users",
|
|
110
|
+
type: "string",
|
|
111
|
+
enum: roles.map((r) => r.role),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
static async system_prompt() {
|
|
117
|
+
return `Use the generate_view tool to generate a view.`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static async follow_on_generate_list({ name, viewpattern, table }) {
|
|
121
|
+
const tbl = Table.findOne({ name: table });
|
|
122
|
+
const triggers = Trigger.find({
|
|
123
|
+
when_trigger: { or: ["API call", "Never"] },
|
|
124
|
+
}).filter(
|
|
125
|
+
(tr) =>
|
|
126
|
+
tr.description && tr.name && (!tr.table_id || tr.table_id === tbl.id)
|
|
127
|
+
);
|
|
128
|
+
const own_show_views = await View.find_table_views_where(
|
|
129
|
+
tbl.id,
|
|
130
|
+
({ state_fields, viewtemplate, viewrow }) =>
|
|
131
|
+
state_fields.some((sf) => sf.name === "id")
|
|
132
|
+
);
|
|
133
|
+
const prompt = `Now generate the structure of the ${name} ${viewpattern} view on the ${table} table`;
|
|
134
|
+
const response_schema = {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
columns: {
|
|
138
|
+
type: "array",
|
|
139
|
+
items: {
|
|
140
|
+
anyOf: [
|
|
141
|
+
{
|
|
142
|
+
type: "object",
|
|
143
|
+
description: "Show a field value",
|
|
144
|
+
//required: ["name", "title", "min_role"],
|
|
145
|
+
properties: {
|
|
146
|
+
coltype: { const: "Field" },
|
|
147
|
+
fieldSpec: {
|
|
148
|
+
anyOf: tbl.fields.map((f) => ({
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
fieldname: { const: f.name },
|
|
152
|
+
fieldview: {
|
|
153
|
+
type: "string",
|
|
154
|
+
enum: Object.keys(f.type?.fieldviews || {}),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
})),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: "object",
|
|
163
|
+
description: "Action button",
|
|
164
|
+
//required: ["name", "title", "min_role"],
|
|
165
|
+
properties: {
|
|
166
|
+
coltype: { const: "Action" },
|
|
167
|
+
label: { type: "string", description: "The button label" },
|
|
168
|
+
style: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "The bootstrap button class",
|
|
171
|
+
enum: [
|
|
172
|
+
"btn-primary",
|
|
173
|
+
"btn-secondary",
|
|
174
|
+
"btn-outline-primary",
|
|
175
|
+
"btn-outline-secondary",
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
action: {
|
|
179
|
+
anyOf: [
|
|
180
|
+
{
|
|
181
|
+
type: "object",
|
|
182
|
+
description: "Delete this row form the database table",
|
|
183
|
+
properties: {
|
|
184
|
+
action_name: { const: "Delete" },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
...triggers.map((tr) => ({
|
|
188
|
+
type: "object",
|
|
189
|
+
description: tr.description,
|
|
190
|
+
properties: {
|
|
191
|
+
action_name: { const: tr.name },
|
|
192
|
+
},
|
|
193
|
+
})),
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: "object",
|
|
200
|
+
description: "a link to a different view on the same table",
|
|
201
|
+
properties: {
|
|
202
|
+
coltype: { const: "View link" },
|
|
203
|
+
label: { type: "string", description: "The view label" },
|
|
204
|
+
style: {
|
|
205
|
+
type: "string",
|
|
206
|
+
description:
|
|
207
|
+
"Link for a normal link, or for a button, the bootstrap button class",
|
|
208
|
+
enum: [
|
|
209
|
+
"Link",
|
|
210
|
+
"btn-primary",
|
|
211
|
+
"btn-secondary",
|
|
212
|
+
"btn-outline-primary",
|
|
213
|
+
"btn-outline-secondary",
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
view: {
|
|
217
|
+
type: "string",
|
|
218
|
+
description: "the view to link to",
|
|
219
|
+
enum: own_show_views.map((v) => v.name),
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
return { response_schema, prompt };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
static async follow_on_generate(properties) {
|
|
232
|
+
switch (properties.viewpattern) {
|
|
233
|
+
case "List":
|
|
234
|
+
default:
|
|
235
|
+
return GenerateView.follow_on_generate_list(properties);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
static render_html(attrs, contents) {
|
|
240
|
+
console.log(contents);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
pre(code(JSON.stringify(attrs, null, 2))) +
|
|
244
|
+
(contents ? pre(code(JSON.stringify(JSON.parse(contents), null, 2))) : "")
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
static async execute({ name, table, viewpattern, min_role }, req, contents) {
|
|
248
|
+
console.log("execute", name, contents);
|
|
249
|
+
const roles = await User.get_roles();
|
|
250
|
+
const min_role_id = min_role
|
|
251
|
+
? roles.find((r) => r.role === min_role).id
|
|
252
|
+
: 100;
|
|
253
|
+
const tbl = Table.findOne({ name: table });
|
|
254
|
+
const viewCfg = {
|
|
255
|
+
table_id: tbl.id,
|
|
256
|
+
name,
|
|
257
|
+
viewtemplate: viewpattern,
|
|
258
|
+
min_role: min_role_id,
|
|
259
|
+
configuration: {},
|
|
260
|
+
};
|
|
261
|
+
switch (viewpattern) {
|
|
262
|
+
case "List":
|
|
263
|
+
const conts = JSON.parse(contents);
|
|
264
|
+
const cols = conts.columns;
|
|
265
|
+
const segments = cols.map((c) => col2layoutSegment(tbl, c));
|
|
266
|
+
viewCfg.configuration.layout = {
|
|
267
|
+
besides: segments.map((s) => ({ contents: s })),
|
|
268
|
+
};
|
|
269
|
+
viewCfg.configuration.columns = segments.map(segment2column);
|
|
270
|
+
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
default:
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
console.log(viewCfg);
|
|
277
|
+
|
|
278
|
+
await View.create(viewCfg);
|
|
279
|
+
return {
|
|
280
|
+
postExec:
|
|
281
|
+
"View created. " +
|
|
282
|
+
a(
|
|
283
|
+
{ target: "_blank", href: `/view/${name}`, class: "me-1" },
|
|
284
|
+
"Go to view"
|
|
285
|
+
) +
|
|
286
|
+
" | " +
|
|
287
|
+
a(
|
|
288
|
+
{
|
|
289
|
+
target: "_blank",
|
|
290
|
+
href: `/viewedit/config/${name}`,
|
|
291
|
+
class: "ms-1",
|
|
292
|
+
},
|
|
293
|
+
"Configure view"
|
|
294
|
+
),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = GenerateView;
|
|
@@ -16,7 +16,9 @@ const steps = async () => {
|
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
const stepTypeAndCfg = Object.keys(actionExplainers).map((actionName) => {
|
|
19
|
-
const properties = {
|
|
19
|
+
const properties = {
|
|
20
|
+
step_type: { type: "string", enum: [actionName] }
|
|
21
|
+
};
|
|
20
22
|
const myFields = actionFields.filter(
|
|
21
23
|
(f) => f.showIf?.wf_action_name === actionName
|
|
22
24
|
);
|
|
@@ -37,7 +39,9 @@ const steps = async () => {
|
|
|
37
39
|
});
|
|
38
40
|
for (const [actionName, action] of stateActionList) {
|
|
39
41
|
try {
|
|
40
|
-
const properties = {
|
|
42
|
+
const properties = {
|
|
43
|
+
step_type: { type: "string", enum: [actionName] }
|
|
44
|
+
};
|
|
41
45
|
const cfgFields = await getActionConfigFields(action, null, {
|
|
42
46
|
mode: "workflow",
|
|
43
47
|
copilot: true,
|
|
@@ -69,7 +73,10 @@ const steps = async () => {
|
|
|
69
73
|
//TODO workflows
|
|
70
74
|
for (const trigger of triggers) {
|
|
71
75
|
const properties = {
|
|
72
|
-
step_type: {
|
|
76
|
+
step_type: {
|
|
77
|
+
type: "string",
|
|
78
|
+
enum: [trigger.name],
|
|
79
|
+
},
|
|
73
80
|
};
|
|
74
81
|
if (trigger.table_id) {
|
|
75
82
|
const table = Table.findOne({ id: trigger.table_id });
|
package/chat-copilot.js
CHANGED
|
@@ -142,7 +142,9 @@ const run = async (table_id, viewname, cfg, state, { res, req }) => {
|
|
|
142
142
|
i(
|
|
143
143
|
small(
|
|
144
144
|
"Skills you can request: " +
|
|
145
|
-
|
|
145
|
+
classesWithSkills()
|
|
146
|
+
.map((ac) => ac.title)
|
|
147
|
+
.join(", ")
|
|
146
148
|
)
|
|
147
149
|
)
|
|
148
150
|
);
|
|
@@ -292,12 +294,20 @@ const actionClasses = [
|
|
|
292
294
|
require("./actions/generate-workflow"),
|
|
293
295
|
require("./actions/generate-tables"),
|
|
294
296
|
require("./actions/generate-js-action"),
|
|
297
|
+
require("./actions/generate-page"),
|
|
298
|
+
require("./actions/generate-view"),
|
|
295
299
|
];
|
|
296
300
|
|
|
301
|
+
const classesWithSkills = () => {
|
|
302
|
+
const state = getState();
|
|
303
|
+
const skills = state.copilot_skills || [];
|
|
304
|
+
return [...actionClasses, ...skills];
|
|
305
|
+
};
|
|
306
|
+
|
|
297
307
|
const getCompletionArguments = async () => {
|
|
298
308
|
const tools = [];
|
|
299
309
|
const sysPrompts = [];
|
|
300
|
-
for (const actionClass of
|
|
310
|
+
for (const actionClass of classesWithSkills()) {
|
|
301
311
|
tools.push({
|
|
302
312
|
type: "function",
|
|
303
313
|
function: {
|
|
@@ -326,10 +336,25 @@ const execute = async (table_id, viewname, config, body, { req }) => {
|
|
|
326
336
|
const run = await WorkflowRun.findOne({ id: +run_id });
|
|
327
337
|
|
|
328
338
|
const fcall = run.context.funcalls[fcall_id];
|
|
329
|
-
const actionClass =
|
|
339
|
+
const actionClass = classesWithSkills().find(
|
|
330
340
|
(ac) => ac.function_name === fcall.name
|
|
331
341
|
);
|
|
332
|
-
|
|
342
|
+
let result;
|
|
343
|
+
if (actionClass.follow_on_generate) {
|
|
344
|
+
const toolCallIndex = run.context.interactions.findIndex(
|
|
345
|
+
(i) => i.tool_call_id === fcall_id
|
|
346
|
+
);
|
|
347
|
+
const follow_on_gen = run.context.interactions.find(
|
|
348
|
+
(i, ix) => i.role === "assistant" && ix > toolCallIndex
|
|
349
|
+
);
|
|
350
|
+
result = await actionClass.execute(
|
|
351
|
+
JSON.parse(fcall.arguments),
|
|
352
|
+
req,
|
|
353
|
+
follow_on_gen.content
|
|
354
|
+
);
|
|
355
|
+
} else {
|
|
356
|
+
result = await actionClass.execute(JSON.parse(fcall.arguments), req);
|
|
357
|
+
}
|
|
333
358
|
await addToContext(run, { implemented_fcall_ids: [fcall_id] });
|
|
334
359
|
return { json: { success: "ok", fcall_id, ...(result || {}) } };
|
|
335
360
|
};
|
|
@@ -387,9 +412,50 @@ const interact = async (table_id, viewname, config, body, { req }) => {
|
|
|
387
412
|
await addToContext(run, {
|
|
388
413
|
funcalls: { [tool_call.id]: tool_call.function },
|
|
389
414
|
});
|
|
390
|
-
const markup = await renderToolcall(tool_call, viewname, false, run);
|
|
391
415
|
|
|
392
|
-
|
|
416
|
+
const followOnGen = await getFollowOnGeneration(tool_call);
|
|
417
|
+
if (followOnGen) {
|
|
418
|
+
const { response_schema, prompt } = followOnGen;
|
|
419
|
+
const follow_on_answer = await getState().functions.llm_generate.run(
|
|
420
|
+
prompt,
|
|
421
|
+
{
|
|
422
|
+
debugResult: true,
|
|
423
|
+
chat: run.context.interactions,
|
|
424
|
+
response_format: response_schema
|
|
425
|
+
? {
|
|
426
|
+
type: "json_schema",
|
|
427
|
+
json_schema: {
|
|
428
|
+
name: "generate_page",
|
|
429
|
+
schema: response_schema,
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
: undefined,
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
console.log("follow on answer", follow_on_answer);
|
|
437
|
+
|
|
438
|
+
await addToContext(run, {
|
|
439
|
+
interactions: [
|
|
440
|
+
{ role: "user", content: prompt },
|
|
441
|
+
{ role: "assistant", content: follow_on_answer },
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const markup = await renderToolcall(
|
|
446
|
+
tool_call,
|
|
447
|
+
viewname,
|
|
448
|
+
false,
|
|
449
|
+
run,
|
|
450
|
+
follow_on_answer
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
actions.push(markup);
|
|
454
|
+
} else {
|
|
455
|
+
const markup = await renderToolcall(tool_call, viewname, false, run);
|
|
456
|
+
|
|
457
|
+
actions.push(markup);
|
|
458
|
+
}
|
|
393
459
|
}
|
|
394
460
|
return { json: { success: "ok", actions, run_id: run.id } };
|
|
395
461
|
} else
|
|
@@ -398,12 +464,32 @@ const interact = async (table_id, viewname, config, body, { req }) => {
|
|
|
398
464
|
};
|
|
399
465
|
};
|
|
400
466
|
|
|
401
|
-
const
|
|
467
|
+
const getFollowOnGeneration = async (tool_call) => {
|
|
468
|
+
const fname = tool_call.function.name;
|
|
469
|
+
const actionClass = classesWithSkills().find(
|
|
470
|
+
(ac) => ac.function_name === fname
|
|
471
|
+
);
|
|
472
|
+
const args = JSON.parse(tool_call.function.arguments);
|
|
473
|
+
|
|
474
|
+
if (actionClass.follow_on_generate) {
|
|
475
|
+
return await actionClass.follow_on_generate(args);
|
|
476
|
+
} else return null;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const renderToolcall = async (
|
|
480
|
+
tool_call,
|
|
481
|
+
viewname,
|
|
482
|
+
implemented,
|
|
483
|
+
run,
|
|
484
|
+
follow_on_answer
|
|
485
|
+
) => {
|
|
402
486
|
const fname = tool_call.function.name;
|
|
403
|
-
const actionClass =
|
|
487
|
+
const actionClass = classesWithSkills().find(
|
|
488
|
+
(ac) => ac.function_name === fname
|
|
489
|
+
);
|
|
404
490
|
const args = JSON.parse(tool_call.function.arguments);
|
|
405
491
|
|
|
406
|
-
const inner_markup = await actionClass.render_html(args);
|
|
492
|
+
const inner_markup = await actionClass.render_html(args, follow_on_answer);
|
|
407
493
|
return wrapAction(
|
|
408
494
|
inner_markup,
|
|
409
495
|
viewname,
|
package/common.js
CHANGED
|
@@ -8,6 +8,79 @@ 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
10
|
|
|
11
|
+
const parseCSS = require("style-to-object").default;
|
|
12
|
+
const MarkdownIt = require("markdown-it"),
|
|
13
|
+
md = new MarkdownIt();
|
|
14
|
+
const HTMLParser = require("node-html-parser");
|
|
15
|
+
|
|
16
|
+
const boxHandledStyles = new Set([
|
|
17
|
+
"margin",
|
|
18
|
+
"margin-top",
|
|
19
|
+
"margin-bottom",
|
|
20
|
+
"margin-right",
|
|
21
|
+
"margin-left",
|
|
22
|
+
"padding",
|
|
23
|
+
"padding-top",
|
|
24
|
+
"padding-bottom",
|
|
25
|
+
"padding-right",
|
|
26
|
+
"padding-left",
|
|
27
|
+
"border-color",
|
|
28
|
+
"border-color",
|
|
29
|
+
"border-width",
|
|
30
|
+
"border-radius",
|
|
31
|
+
"height",
|
|
32
|
+
"min-height",
|
|
33
|
+
"max-height",
|
|
34
|
+
"width",
|
|
35
|
+
"min-width",
|
|
36
|
+
"max-width",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const containerHandledStyles = new Set([
|
|
40
|
+
...boxHandledStyles,
|
|
41
|
+
"opacity",
|
|
42
|
+
"position",
|
|
43
|
+
"top",
|
|
44
|
+
"right",
|
|
45
|
+
"bottom",
|
|
46
|
+
"left",
|
|
47
|
+
"font-family",
|
|
48
|
+
"font-size",
|
|
49
|
+
"font-weight",
|
|
50
|
+
"line-height",
|
|
51
|
+
"flex-grow",
|
|
52
|
+
"flex-shrink",
|
|
53
|
+
"flex-direction",
|
|
54
|
+
"flex-wrap",
|
|
55
|
+
"justify-content",
|
|
56
|
+
"align-items",
|
|
57
|
+
"align-content",
|
|
58
|
+
"display",
|
|
59
|
+
"overflow",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const splitContainerStyle = (styleStr) => {
|
|
63
|
+
const style = parseCSS(styleStr);
|
|
64
|
+
const customStyles = [];
|
|
65
|
+
Object.keys(style || {}).forEach((k) => {
|
|
66
|
+
if (containerHandledStyles.has(k)) {
|
|
67
|
+
customStyles.push(`${k}: ${style[k]}`);
|
|
68
|
+
delete style[k];
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const ns = { style, customStyle: customStyles.join("; ") };
|
|
72
|
+
if (ns.style.display) {
|
|
73
|
+
ns.display = ns.style.display;
|
|
74
|
+
delete ns.style.display;
|
|
75
|
+
}
|
|
76
|
+
if (ns.style.overflow) {
|
|
77
|
+
ns.overflow = ns.style.overflow;
|
|
78
|
+
delete ns.style.overflow;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return ns;
|
|
82
|
+
};
|
|
83
|
+
|
|
11
84
|
const getPromptFromTemplate = async (tmplName, userPrompt, extraCtx = {}) => {
|
|
12
85
|
const tables = await Table.find({});
|
|
13
86
|
const context = {
|
|
@@ -99,7 +172,146 @@ const fieldProperties = (field) => {
|
|
|
99
172
|
if (field.options) props.enum = toArrayOfStrings(field.options);
|
|
100
173
|
break;
|
|
101
174
|
}
|
|
175
|
+
if (!props.type) {
|
|
176
|
+
switch (field.input_type) {
|
|
177
|
+
case "code":
|
|
178
|
+
props.type = "string";
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
102
182
|
return props;
|
|
103
183
|
};
|
|
104
184
|
|
|
105
|
-
|
|
185
|
+
function walk_response(segment) {
|
|
186
|
+
let go = walk_response;
|
|
187
|
+
if (!segment) return segment;
|
|
188
|
+
if (typeof segment === "string") return segment;
|
|
189
|
+
if (segment.element) return go(segment.element);
|
|
190
|
+
if (Array.isArray(segment)) {
|
|
191
|
+
return segment.map(go);
|
|
192
|
+
}
|
|
193
|
+
if (typeof segment.contents === "string") {
|
|
194
|
+
return { ...segment, contents: md.render(segment.contents) };
|
|
195
|
+
}
|
|
196
|
+
if (segment.type === "image") {
|
|
197
|
+
return {
|
|
198
|
+
type: "container",
|
|
199
|
+
style: {
|
|
200
|
+
height: `${segment.height}px`,
|
|
201
|
+
width: `${segment.width}px`,
|
|
202
|
+
"border-style": "solid",
|
|
203
|
+
"border-color": "#808080",
|
|
204
|
+
"border-width": "3px",
|
|
205
|
+
vAlign: "middle",
|
|
206
|
+
hAlign: "center",
|
|
207
|
+
},
|
|
208
|
+
contents: segment.description,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (segment.type === "container") {
|
|
212
|
+
const { customStyle, style, display, overflow } = splitContainerStyle(
|
|
213
|
+
segment.style
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
...segment,
|
|
217
|
+
customStyle,
|
|
218
|
+
display,
|
|
219
|
+
overflow,
|
|
220
|
+
style,
|
|
221
|
+
contents: go(segment.contents),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (segment.contents) {
|
|
225
|
+
return { ...segment, contents: go(segment.contents) };
|
|
226
|
+
}
|
|
227
|
+
if (segment.above) {
|
|
228
|
+
return { ...segment, above: go(segment.above) };
|
|
229
|
+
}
|
|
230
|
+
if (segment.besides) {
|
|
231
|
+
return { ...segment, besides: go(segment.besides) };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseHTML(str) {
|
|
236
|
+
const strHtml = str.includes("```html")
|
|
237
|
+
? str.split("```html")[1].split("```")[0]
|
|
238
|
+
: str;
|
|
239
|
+
const body = HTMLParser.parse(strHtml).querySelector("body");
|
|
240
|
+
|
|
241
|
+
const go = (node) => {
|
|
242
|
+
if (node.constructor.name === "HTMLElement") {
|
|
243
|
+
switch (node.rawTagName) {
|
|
244
|
+
case "body":
|
|
245
|
+
return { above: node.childNodes.map(go).filter(Boolean) };
|
|
246
|
+
case "p":
|
|
247
|
+
return {
|
|
248
|
+
type: "blank",
|
|
249
|
+
contents: node.childNodes.map((n) => n.toString()).join(""),
|
|
250
|
+
customClass: (node.classList.value || []).join(" "),
|
|
251
|
+
};
|
|
252
|
+
case "script":
|
|
253
|
+
return null;
|
|
254
|
+
case "a":
|
|
255
|
+
return {
|
|
256
|
+
type: "link",
|
|
257
|
+
url: node.getAttribute("href"),
|
|
258
|
+
text: node.childNodes.map((n) => n.toString()).join(""),
|
|
259
|
+
link_class: (node.classList.value || []).join(" "),
|
|
260
|
+
link_src: "URL",
|
|
261
|
+
};
|
|
262
|
+
case "img":
|
|
263
|
+
return {
|
|
264
|
+
type: "container",
|
|
265
|
+
style: {
|
|
266
|
+
"border-color": "#808080",
|
|
267
|
+
"border-style": "solid",
|
|
268
|
+
"border-width": "2px",
|
|
269
|
+
},
|
|
270
|
+
contents: {
|
|
271
|
+
type: "blank",
|
|
272
|
+
contents: "Image: " + node.getAttribute("alt"),
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
case "h1":
|
|
277
|
+
case "h2":
|
|
278
|
+
case "h3":
|
|
279
|
+
case "h4":
|
|
280
|
+
case "h5":
|
|
281
|
+
case "h6":
|
|
282
|
+
return {
|
|
283
|
+
type: "blank",
|
|
284
|
+
contents: node.childNodes.map((n) => n.toString()).join(""),
|
|
285
|
+
customClass: (node.classList.value || []).join(" "),
|
|
286
|
+
textStyle: [node.rawTagName],
|
|
287
|
+
};
|
|
288
|
+
default:
|
|
289
|
+
return {
|
|
290
|
+
type: "container",
|
|
291
|
+
htmlElement: node.rawTagName,
|
|
292
|
+
customClass: (node.classList.value || []).join(" "),
|
|
293
|
+
contents: node.childNodes.map(go).filter(Boolean),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
} else if (node.constructor.name === "TextNode") {
|
|
297
|
+
if (!node._rawText || !node._rawText.trim()) return null;
|
|
298
|
+
else return { type: "blank", contents: node._rawText };
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
//console.log(body.constructor.name);
|
|
302
|
+
|
|
303
|
+
//console.log(JSON.stringify(go(body.childNodes[3]), null, 2));
|
|
304
|
+
return go(body);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = {
|
|
308
|
+
getCompletion,
|
|
309
|
+
getPromptFromTemplate,
|
|
310
|
+
incompleteCfgMsg,
|
|
311
|
+
fieldProperties,
|
|
312
|
+
boxHandledStyles,
|
|
313
|
+
containerHandledStyles,
|
|
314
|
+
splitContainerStyle,
|
|
315
|
+
walk_response,
|
|
316
|
+
parseHTML,
|
|
317
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/copilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "AI assistant for building Saltcorn applications",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@saltcorn/data": "^0.9.0",
|
|
8
8
|
"underscore": "1.13.6",
|
|
9
9
|
"node-sql-parser": "4.15.0",
|
|
10
|
-
"markdown-it": "14.1.0"
|
|
10
|
+
"markdown-it": "14.1.0",
|
|
11
|
+
"style-to-object": "1.0.8",
|
|
12
|
+
"node-html-parser": "7.0.1"
|
|
11
13
|
},
|
|
12
14
|
"author": "Tom Nielsen",
|
|
13
15
|
"license": "MIT",
|