@saltcorn/copilot 0.4.5 → 0.5.1
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-view.js +299 -0
- package/chat-copilot.js +95 -9
- package/common.js +200 -0
- package/package.json +4 -2
- package/user-copilot.js +53 -55
|
@@ -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;
|
|
@@ -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;
|
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 = {
|
|
@@ -109,9 +182,136 @@ const fieldProperties = (field) => {
|
|
|
109
182
|
return props;
|
|
110
183
|
};
|
|
111
184
|
|
|
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
|
+
|
|
112
307
|
module.exports = {
|
|
113
308
|
getCompletion,
|
|
114
309
|
getPromptFromTemplate,
|
|
115
310
|
incompleteCfgMsg,
|
|
116
311
|
fieldProperties,
|
|
312
|
+
boxHandledStyles,
|
|
313
|
+
containerHandledStyles,
|
|
314
|
+
splitContainerStyle,
|
|
315
|
+
walk_response,
|
|
316
|
+
parseHTML,
|
|
117
317
|
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/copilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
package/user-copilot.js
CHANGED
|
@@ -466,39 +466,42 @@ const run = async (table_id, viewname, config, state, { res, req }) => {
|
|
|
466
466
|
const getCompletionArguments = async (config) => {
|
|
467
467
|
let tools = [];
|
|
468
468
|
const sysPrompts = [];
|
|
469
|
-
let properties = {};
|
|
470
469
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
470
|
+
if (config.tables.length) {
|
|
471
|
+
let properties = {};
|
|
472
|
+
|
|
473
|
+
const tableNames = (config?.tables || []).map((t) => t.table_name);
|
|
474
|
+
properties.table_name = {
|
|
475
|
+
type: "string",
|
|
476
|
+
enum: tableNames,
|
|
477
|
+
description: `Which table is this query from. Every query has to select rows from one table, even if it is based on joins from different tables`,
|
|
478
|
+
};
|
|
479
|
+
properties.sql_id_query = {
|
|
480
|
+
type: "string",
|
|
481
|
+
description: `An SQL query for this table's primary keys. This must select only the primary keys (even if the user wants a count), for example SELECT ${
|
|
482
|
+
tableNames[0][0]
|
|
483
|
+
}."${Table.findOne(tableNames[0]).pk_name}" from "${tableNames[0]}" ${
|
|
484
|
+
tableNames[0][0]
|
|
485
|
+
} JOIN ... where... Use this to join other tables in the database.`,
|
|
486
|
+
};
|
|
487
|
+
properties.is_count = {
|
|
488
|
+
type: "boolean",
|
|
489
|
+
description: `Is the only desired output a count? Make this true if the user wants a count of rows`,
|
|
490
|
+
};
|
|
489
491
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
492
|
+
tools.push({
|
|
493
|
+
type: "function",
|
|
494
|
+
function: {
|
|
495
|
+
name: "TableQuery",
|
|
496
|
+
description: `Query a table and show the results to the user in a grid format`,
|
|
497
|
+
parameters: {
|
|
498
|
+
type: "object",
|
|
499
|
+
required: ["table_name", "sql_id_query", "is_count"],
|
|
500
|
+
properties,
|
|
501
|
+
},
|
|
499
502
|
},
|
|
500
|
-
}
|
|
501
|
-
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
502
505
|
|
|
503
506
|
for (const action of config?.actions || []) {
|
|
504
507
|
let properties = {};
|
|
@@ -536,18 +539,19 @@ const getCompletionArguments = async (config) => {
|
|
|
536
539
|
const systemPrompt =
|
|
537
540
|
"You are helping users retrieve information and perform actions on a relational database" +
|
|
538
541
|
config.sys_prompt +
|
|
539
|
-
|
|
540
|
-
|
|
542
|
+
(config.tables.length
|
|
543
|
+
? `
|
|
544
|
+
If you are generating SQL, Your database has the following tables in PostgreSQL:
|
|
541
545
|
|
|
542
546
|
` +
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
547
|
+
tables
|
|
548
|
+
.map(
|
|
549
|
+
(t) => `CREATE TABLE "${t.name}" (${
|
|
550
|
+
t.description
|
|
551
|
+
? `
|
|
548
552
|
/* ${t.description} */`
|
|
549
|
-
|
|
550
|
-
|
|
553
|
+
: ""
|
|
554
|
+
}
|
|
551
555
|
${t.fields
|
|
552
556
|
.map(
|
|
553
557
|
(f) =>
|
|
@@ -559,15 +563,16 @@ ${t.fields
|
|
|
559
563
|
)
|
|
560
564
|
.join(",\n")}
|
|
561
565
|
)`
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
566
|
+
)
|
|
567
|
+
.join(";\n\n") +
|
|
568
|
+
`
|
|
565
569
|
|
|
566
570
|
Use the TableQuery tool if the user asks to see, find or count or otherwise access rows from a table that matches what the user is looking for, or if
|
|
567
571
|
the user is asking for a summary or inference from such rows. The TableQuery query is parametrised by a SQL SELECT query which
|
|
568
572
|
selects primary key values from the specified table. You can join other tables or use complex logic in the WHERE clause, but you must
|
|
569
573
|
always return porimary key values from the specified table.
|
|
570
|
-
|
|
574
|
+
`
|
|
575
|
+
: "");
|
|
571
576
|
//console.log("sysprompt", systemPrompt);
|
|
572
577
|
|
|
573
578
|
if (tools.length === 0) tools = undefined;
|
|
@@ -600,7 +605,7 @@ const interact = async (table_id, viewname, config, body, { req, res }) => {
|
|
|
600
605
|
interactions: [{ role: "user", content: userinput }],
|
|
601
606
|
});
|
|
602
607
|
}
|
|
603
|
-
return await process_interaction(run,
|
|
608
|
+
return await process_interaction(run, config, req);
|
|
604
609
|
};
|
|
605
610
|
|
|
606
611
|
const renderQueryInteraction = async (table, result, config, req) => {
|
|
@@ -653,20 +658,13 @@ const renderQueryInteraction = async (table, result, config, req) => {
|
|
|
653
658
|
);
|
|
654
659
|
};
|
|
655
660
|
|
|
656
|
-
const process_interaction = async (
|
|
657
|
-
run,
|
|
658
|
-
input,
|
|
659
|
-
config,
|
|
660
|
-
req,
|
|
661
|
-
prevResponses = []
|
|
662
|
-
) => {
|
|
661
|
+
const process_interaction = async (run, config, req, prevResponses = []) => {
|
|
663
662
|
const complArgs = await getCompletionArguments(config);
|
|
664
663
|
complArgs.chat = run.context.interactions;
|
|
665
664
|
//complArgs.debugResult = true;
|
|
666
|
-
//console.log(complArgs);
|
|
667
|
-
console.log("complArgs", JSON.stringify(complArgs, null, 2));
|
|
665
|
+
//console.log("complArgs", JSON.stringify(complArgs, null, 2));
|
|
668
666
|
|
|
669
|
-
const answer = await getState().functions.llm_generate.run(
|
|
667
|
+
const answer = await getState().functions.llm_generate.run("", complArgs);
|
|
670
668
|
console.log("answer", answer);
|
|
671
669
|
await addToContext(run, {
|
|
672
670
|
interactions:
|
|
@@ -820,7 +818,7 @@ const process_interaction = async (
|
|
|
820
818
|
}
|
|
821
819
|
}
|
|
822
820
|
if (hasResult)
|
|
823
|
-
return await process_interaction(run,
|
|
821
|
+
return await process_interaction(run, config, req, [
|
|
824
822
|
...prevResponses,
|
|
825
823
|
...responses,
|
|
826
824
|
]);
|