@saltcorn/copilot 0.7.5 → 0.8.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-js-action.js +43 -5
- package/actions/generate-tables.js +281 -96
- package/actions/generate-workflow.js +83 -36
- package/actions/install-plugin-action.js +103 -0
- package/agent-skills/app-constructor-context.js +25 -0
- package/agent-skills/database-design.js +139 -87
- package/agent-skills/install-plugin.js +111 -0
- package/agent-skills/js-action.js +183 -0
- package/agent-skills/registry-editor.js +911 -0
- package/agent-skills/viewgen.js +239 -21
- package/agent-skills/workflow.js +52 -2
- package/app-constructor/common.js +12 -0
- package/app-constructor/errors.js +102 -0
- package/app-constructor/feedback-action.js +175 -0
- package/app-constructor/feedback.js +112 -0
- package/app-constructor/progress.js +116 -0
- package/app-constructor/prompts.js +58 -0
- package/app-constructor/requirements.js +156 -0
- package/app-constructor/run_task.js +128 -0
- package/app-constructor/schema.js +186 -0
- package/app-constructor/taskchart.js +70 -0
- package/app-constructor/tasks.js +401 -0
- package/app-constructor/tools.js +81 -0
- package/app-constructor/view.js +209 -0
- package/builder-gen.js +534 -66
- package/builder-schema.js +26 -6
- package/common.js +20 -0
- package/copilot-as-agent.js +6 -1
- package/index.js +23 -1
- package/js-code-gen.js +65 -0
- package/package.json +1 -1
- package/tests/builder-gen.test.js +56 -0
package/builder-gen.js
CHANGED
|
@@ -4,15 +4,102 @@ const Trigger = require("@saltcorn/data/models/trigger");
|
|
|
4
4
|
const View = require("@saltcorn/data/models/view");
|
|
5
5
|
const { edit_build_in_actions } = require("@saltcorn/data/viewable_fields");
|
|
6
6
|
const { buildBuilderSchema } = require("./builder-schema");
|
|
7
|
+
const { getLlmConfigurationSafe, canUseResponseFormat } = require("./common");
|
|
8
|
+
const { build_schema_data } = require("@saltcorn/data/plugin-helper");
|
|
9
|
+
const { RelationsFinder } = require("@saltcorn/common-code");
|
|
10
|
+
const { RelationType } = require("@saltcorn/common-code");
|
|
7
11
|
|
|
8
12
|
const ACTION_SIZES = ["btn-sm", "btn-lg"];
|
|
13
|
+
|
|
14
|
+
// ── Edit-mode guidance blocks ─────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const EDIT_FIELD_INCLUSION = `\
|
|
17
|
+
Layout is a form for editing a single row. \
|
|
18
|
+
Include ALL non-primary-key, non-calculated fields from the table as inputs — \
|
|
19
|
+
not just fields mentioned in the task description. \
|
|
20
|
+
Never include calculated fields (aggregations, derived values) in an edit form — they are read-only. \
|
|
21
|
+
The only fields that may be omitted are FK fields to the users table that are explicitly \
|
|
22
|
+
designated as ownership fields (set automatically from the logged-in user). \
|
|
23
|
+
All other foreign key fields — including parent-context keys like trip_id — MUST appear as \
|
|
24
|
+
selector inputs; Saltcorn auto-fills them from the URL state when opened in context.`;
|
|
25
|
+
|
|
26
|
+
const EDIT_FIELDVIEW_SELECTION = `\
|
|
27
|
+
For date fields always prefer fieldview "flatpickr" when available — it provides the best user experience \
|
|
28
|
+
and works for both regular dates and day-only dates. \
|
|
29
|
+
Only use fieldview "edit_day" as a fallback when the field has day_only=true and flatpickr is not installed. \
|
|
30
|
+
For String fields that have an options attribute (a comma-separated list of fixed choices), \
|
|
31
|
+
use fieldview "select" — this renders a dropdown with those options. \
|
|
32
|
+
Do not use "select_by_code" for fields with fixed options.`;
|
|
33
|
+
|
|
34
|
+
const EDIT_LAYOUT_STRUCTURE = `\
|
|
35
|
+
Every field MUST be preceded by a label. \
|
|
36
|
+
Use a blank segment with the field's label text immediately above each field inside an above array, like: \
|
|
37
|
+
{"above": [{"type":"blank","contents":"Field Label","class":""}, {"type":"field","field_name":"...","fieldview":"edit"}]}. \
|
|
38
|
+
Alternatively wrap groups of fields in a card with a descriptive title.`;
|
|
39
|
+
|
|
40
|
+
const EDIT_ACTIONS = `\
|
|
41
|
+
Use edit fieldviews, group related inputs, and finish with a row of actions: \
|
|
42
|
+
a Save button (action_name "Save", style "btn btn-primary") and \
|
|
43
|
+
a Cancel button (action_name "GoBack", style "btn btn-outline-secondary").`;
|
|
44
|
+
|
|
45
|
+
// ── Show-mode guidance blocks ─────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const SHOW_PURPOSE = `\
|
|
48
|
+
Layout displays one record read-only. \
|
|
49
|
+
Use show fieldviews only (e.g. "show", "as_text", "showDay").`;
|
|
50
|
+
|
|
51
|
+
const SHOW_NO_EDIT_FIELDVIEWS = `\
|
|
52
|
+
A show layout is read-only and must never use edit fieldviews \
|
|
53
|
+
(textarea, edit, input, select) — those are for form inputs, not display.`;
|
|
54
|
+
|
|
55
|
+
const SHOW_LABELS = `\
|
|
56
|
+
Every field MUST be preceded by a label using a blank segment or \
|
|
57
|
+
grouped in a card with a descriptive title.`;
|
|
58
|
+
|
|
59
|
+
// ── List-columns guidance blocks ──────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
const LISTCOLUMNS_STRUCTURE = `\
|
|
62
|
+
Layout defines list columns for a list view. \
|
|
63
|
+
Use a list_columns wrapper with besides entries containing list_column segments. \
|
|
64
|
+
Each list_column has a header_label for the column heading — \
|
|
65
|
+
do NOT add any blank or label segment inside a list_column. \
|
|
66
|
+
Typically each list_column contains a single field, action, or view_link.`;
|
|
67
|
+
|
|
68
|
+
const LISTCOLUMNS_FIELDVIEWS = `\
|
|
69
|
+
Include all meaningful fields as columns, including foreign key fields. \
|
|
70
|
+
For a foreign key field use fieldview "show" so the related record's label is displayed rather than a raw ID. \
|
|
71
|
+
For string/text fields use fieldview "as_text" or "show" by default; \
|
|
72
|
+
only use "show_with_html" when the task explicitly requires rendering HTML content.`;
|
|
73
|
+
|
|
74
|
+
const LISTCOLUMNS_VIEW_LINK = `\
|
|
75
|
+
A view_link that opens the detail of a row MUST point to a Show-type view (viewtemplate "Show"). \
|
|
76
|
+
Only include such a view_link if a Show view for this table is listed in the available views.`;
|
|
77
|
+
|
|
78
|
+
// ── Composed MODE_GUIDANCE ────────────────────────────────────────────────────
|
|
79
|
+
|
|
9
80
|
const MODE_GUIDANCE = {
|
|
10
|
-
edit:
|
|
11
|
-
|
|
81
|
+
edit: [
|
|
82
|
+
EDIT_FIELD_INCLUSION,
|
|
83
|
+
EDIT_ACTIONS,
|
|
84
|
+
EDIT_FIELDVIEW_SELECTION,
|
|
85
|
+
EDIT_LAYOUT_STRUCTURE,
|
|
86
|
+
].join(" "),
|
|
87
|
+
|
|
88
|
+
show: [SHOW_PURPOSE, SHOW_NO_EDIT_FIELDVIEWS, SHOW_LABELS].join(" "),
|
|
89
|
+
|
|
12
90
|
list: "Layout represents a single row in a list. Highlight key fields, keep actions compact, and support filtering if requested.",
|
|
91
|
+
|
|
92
|
+
listcolumns: [
|
|
93
|
+
LISTCOLUMNS_STRUCTURE,
|
|
94
|
+
LISTCOLUMNS_FIELDVIEWS,
|
|
95
|
+
LISTCOLUMNS_VIEW_LINK,
|
|
96
|
+
].join(" "),
|
|
97
|
+
|
|
13
98
|
filter:
|
|
14
99
|
"Layout lets users define filters. Provide appropriate filter inputs plus an action to run or reset filters.",
|
|
100
|
+
|
|
15
101
|
page: "Layout builds a general app page. Combine hero text, cards, containers, and call-to-action buttons.",
|
|
102
|
+
|
|
16
103
|
default:
|
|
17
104
|
"Use Saltcorn layout primitives (above, besides, container, card, tabs, blank, field, action, view_link, view). Do not return HTML snippets.",
|
|
18
105
|
};
|
|
@@ -188,11 +275,200 @@ const derefSchema = (schema, maxDepth = 3) => {
|
|
|
188
275
|
return expand(root, []);
|
|
189
276
|
};
|
|
190
277
|
|
|
278
|
+
const simplifySchemaForLlm = (schema) => {
|
|
279
|
+
const deref = derefSchema(schema, 2); // Reduced maxDepth
|
|
280
|
+
const scrub = (node, keyName, depth = 0) => {
|
|
281
|
+
if (!node || typeof node !== "object" || depth > 4) {
|
|
282
|
+
// Added depth limit
|
|
283
|
+
if (
|
|
284
|
+
keyName === "contents" ||
|
|
285
|
+
keyName === "besides" ||
|
|
286
|
+
keyName === "above"
|
|
287
|
+
)
|
|
288
|
+
return {
|
|
289
|
+
type: "string",
|
|
290
|
+
description: "Simplified content: a string or a basic segment.",
|
|
291
|
+
};
|
|
292
|
+
return node;
|
|
293
|
+
}
|
|
294
|
+
if (Array.isArray(node))
|
|
295
|
+
return node.map((item) => scrub(item, undefined, depth + 1));
|
|
296
|
+
if (keyName === "style") {
|
|
297
|
+
return {
|
|
298
|
+
type: "string",
|
|
299
|
+
description: "CSS style string (e.g. background-color: red;).",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (typeof node.$ref === "string") {
|
|
303
|
+
return {
|
|
304
|
+
type: "object",
|
|
305
|
+
description: "Simplified recursive segment.",
|
|
306
|
+
additionalProperties: false,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const out = {};
|
|
310
|
+
for (const [key, val] of Object.entries(node)) {
|
|
311
|
+
if (key === "$defs" || key === "definitions") continue;
|
|
312
|
+
out[key] = scrub(val, key, depth + 1);
|
|
313
|
+
}
|
|
314
|
+
const nodeType = out.type;
|
|
315
|
+
const isObjectType =
|
|
316
|
+
nodeType === "object" ||
|
|
317
|
+
(Array.isArray(nodeType) && nodeType.includes("object")) ||
|
|
318
|
+
Object.prototype.hasOwnProperty.call(out, "properties");
|
|
319
|
+
const isArrayType =
|
|
320
|
+
nodeType === "array" ||
|
|
321
|
+
(Array.isArray(nodeType) && nodeType.includes("array")) ||
|
|
322
|
+
Object.prototype.hasOwnProperty.call(out, "items");
|
|
323
|
+
const isNumberType =
|
|
324
|
+
nodeType === "number" ||
|
|
325
|
+
nodeType === "integer" ||
|
|
326
|
+
(Array.isArray(nodeType) &&
|
|
327
|
+
(nodeType.includes("number") || nodeType.includes("integer")));
|
|
328
|
+
if (isObjectType) {
|
|
329
|
+
out.additionalProperties = false;
|
|
330
|
+
}
|
|
331
|
+
if (isArrayType && typeof out.minItems !== "undefined") {
|
|
332
|
+
const minItems = Number(out.minItems);
|
|
333
|
+
if (Number.isFinite(minItems) && minItems > 1) out.minItems = 1;
|
|
334
|
+
}
|
|
335
|
+
if (isNumberType) {
|
|
336
|
+
delete out.minimum;
|
|
337
|
+
delete out.maximum;
|
|
338
|
+
delete out.exclusiveMinimum;
|
|
339
|
+
delete out.exclusiveMaximum;
|
|
340
|
+
delete out.multipleOf;
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
};
|
|
344
|
+
return scrub(deref);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const countOptionalParams = (schema) => {
|
|
348
|
+
let count = 0;
|
|
349
|
+
const walk = (node) => {
|
|
350
|
+
if (!node || typeof node !== "object") return;
|
|
351
|
+
if (Array.isArray(node)) {
|
|
352
|
+
node.forEach(walk);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const props = node.properties;
|
|
356
|
+
if (props && typeof props === "object") {
|
|
357
|
+
const required = Array.isArray(node.required) ? node.required : [];
|
|
358
|
+
const optional = Object.keys(props).filter(
|
|
359
|
+
(key) => !required.includes(key)
|
|
360
|
+
);
|
|
361
|
+
count += optional.length;
|
|
362
|
+
Object.values(props).forEach(walk);
|
|
363
|
+
}
|
|
364
|
+
if (node.items) walk(node.items);
|
|
365
|
+
if (node.anyOf) node.anyOf.forEach(walk);
|
|
366
|
+
if (node.oneOf) node.oneOf.forEach(walk);
|
|
367
|
+
if (node.allOf) node.allOf.forEach(walk);
|
|
368
|
+
};
|
|
369
|
+
walk(schema);
|
|
370
|
+
return count;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const trimOptionalProperties = (schema, maxOptional) => {
|
|
374
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
375
|
+
|
|
376
|
+
const clonedSchema = JSON.parse(JSON.stringify(schema));
|
|
377
|
+
let optionalCount = countOptionalParams(clonedSchema);
|
|
378
|
+
if (optionalCount <= maxOptional) {
|
|
379
|
+
return clonedSchema;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const optionalPropsByNode = new Map();
|
|
383
|
+
|
|
384
|
+
const findOptional = (node, path = []) => {
|
|
385
|
+
if (!node || typeof node !== "object") return;
|
|
386
|
+
if (Array.isArray(node)) {
|
|
387
|
+
node.forEach((item, i) => findOptional(item, [...path, i]));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const props = node.properties;
|
|
392
|
+
if (props && typeof props === "object") {
|
|
393
|
+
const required = new Set(node.required || []);
|
|
394
|
+
const optionalKeys = Object.keys(props).filter((k) => !required.has(k));
|
|
395
|
+
if (optionalKeys.length > 0) {
|
|
396
|
+
optionalPropsByNode.set(node, optionalKeys);
|
|
397
|
+
}
|
|
398
|
+
Object.values(props).forEach((propNode) => findOptional(propNode));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (node.items) findOptional(node.items, [...path, "items"]);
|
|
402
|
+
if (node.anyOf)
|
|
403
|
+
node.anyOf.forEach((item, i) =>
|
|
404
|
+
findOptional(item, [...path, "anyOf", i])
|
|
405
|
+
);
|
|
406
|
+
if (node.oneOf)
|
|
407
|
+
node.oneOf.forEach((item, i) =>
|
|
408
|
+
findOptional(item, [...path, "oneOf", i])
|
|
409
|
+
);
|
|
410
|
+
if (node.allOf)
|
|
411
|
+
node.allOf.forEach((item, i) =>
|
|
412
|
+
findOptional(item, [...path, "allOf", i])
|
|
413
|
+
);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
findOptional(clonedSchema);
|
|
417
|
+
|
|
418
|
+
const nodesWithOptionals = Array.from(optionalPropsByNode.keys());
|
|
419
|
+
let nodeIndex = 0;
|
|
420
|
+
|
|
421
|
+
while (optionalCount > maxOptional && nodeIndex < nodesWithOptionals.length) {
|
|
422
|
+
const node = nodesWithOptionals[nodeIndex];
|
|
423
|
+
const optionalKeys = optionalPropsByNode.get(node);
|
|
424
|
+
|
|
425
|
+
if (optionalKeys && optionalKeys.length > 0) {
|
|
426
|
+
const keyToRemove = optionalKeys.pop();
|
|
427
|
+
delete node.properties[keyToRemove];
|
|
428
|
+
optionalCount--;
|
|
429
|
+
} else {
|
|
430
|
+
nodeIndex++;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return clonedSchema;
|
|
435
|
+
};
|
|
436
|
+
|
|
191
437
|
const randomId = () =>
|
|
192
438
|
Math.floor(Math.random() * 0xffffff)
|
|
193
439
|
.toString(16)
|
|
194
440
|
.padStart(6, "0");
|
|
195
441
|
|
|
442
|
+
const toCamelCaseStyle = (key) =>
|
|
443
|
+
key.replace(/-([a-z])/g, (_m, chr) => chr.toUpperCase());
|
|
444
|
+
|
|
445
|
+
const parseStyleString = (styleStr) => {
|
|
446
|
+
if (typeof styleStr !== "string") return styleStr;
|
|
447
|
+
const trimmed = styleStr.trim();
|
|
448
|
+
if (!trimmed) return {};
|
|
449
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
450
|
+
try {
|
|
451
|
+
const parsed = JSON.parse(trimmed);
|
|
452
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
453
|
+
} catch (err) {
|
|
454
|
+
// fall through to CSS parsing
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const out = {};
|
|
458
|
+
trimmed.split(";").forEach((part) => {
|
|
459
|
+
const section = part.trim();
|
|
460
|
+
if (!section) return;
|
|
461
|
+
const idx = section.indexOf(":");
|
|
462
|
+
if (idx === -1) return;
|
|
463
|
+
const rawKey = section.slice(0, idx).trim();
|
|
464
|
+
const value = section.slice(idx + 1).trim();
|
|
465
|
+
if (!rawKey || !value) return;
|
|
466
|
+
if (rawKey.startsWith("--")) out[rawKey] = value;
|
|
467
|
+
else out[toCamelCaseStyle(rawKey)] = value;
|
|
468
|
+
});
|
|
469
|
+
return out;
|
|
470
|
+
};
|
|
471
|
+
|
|
196
472
|
const ensureArray = (value) =>
|
|
197
473
|
Array.isArray(value) ? value : value == null ? [] : [value];
|
|
198
474
|
|
|
@@ -214,29 +490,60 @@ const pickFieldview = (field, mode, requestedFieldview = null) => {
|
|
|
214
490
|
return mode === "edit" || mode === "filter" ? "edit" : "show";
|
|
215
491
|
}
|
|
216
492
|
|
|
217
|
-
// Helper to validate and return a fieldview only if it exists
|
|
493
|
+
// Helper to validate and return a fieldview only if it exists (exact match only)
|
|
218
494
|
const validateAndReturn = (candidate) => {
|
|
219
495
|
if (!candidate) return null;
|
|
220
496
|
const lower = String(candidate).toLowerCase();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
(fv) => String(fv).toLowerCase() === lower,
|
|
497
|
+
return (
|
|
498
|
+
availableViews.find((fv) => String(fv).toLowerCase() === lower) || null
|
|
224
499
|
);
|
|
225
|
-
if (exact) return exact;
|
|
226
|
-
// Fuzzy match (contains)
|
|
227
|
-
const fuzzy = availableViews.find((fv) =>
|
|
228
|
-
String(fv).toLowerCase().includes(lower),
|
|
229
|
-
);
|
|
230
|
-
if (fuzzy) return fuzzy;
|
|
231
|
-
return null;
|
|
232
500
|
};
|
|
233
501
|
|
|
502
|
+
// Edit-only fieldviews must never be used in show/list modes
|
|
503
|
+
const EDIT_ONLY_FIELDVIEWS = new Set(["textarea", "edit", "input", "select"]);
|
|
504
|
+
const isShowMode =
|
|
505
|
+
mode === "show" || mode === "list" || mode === "listcolumns";
|
|
506
|
+
|
|
507
|
+
// For date fields in edit mode: flatpickr is always preferred (handles all date types).
|
|
508
|
+
// Only fall back to edit_day when day_only=true and flatpickr is not installed.
|
|
509
|
+
const fieldTypeLower = String(field?.type || "").toLowerCase();
|
|
510
|
+
const isDateField =
|
|
511
|
+
(mode === "edit" || mode === "filter") &&
|
|
512
|
+
(fieldTypeLower === "date" || fieldTypeLower.includes("date"));
|
|
513
|
+
if (isDateField) {
|
|
514
|
+
const flatpickrMatch = availableViews.find(
|
|
515
|
+
(fv) => String(fv).toLowerCase() === "flatpickr"
|
|
516
|
+
);
|
|
517
|
+
if (flatpickrMatch) return flatpickrMatch;
|
|
518
|
+
if (field?.attributes?.day_only) {
|
|
519
|
+
const dayMatch = availableViews.find(
|
|
520
|
+
(fv) => String(fv).toLowerCase() === "edit_day"
|
|
521
|
+
);
|
|
522
|
+
if (dayMatch) return dayMatch;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Fields with fixed options (options attribute) always use plain select, never select_by_code
|
|
527
|
+
if ((mode === "edit" || mode === "filter") && field?.attributes?.options) {
|
|
528
|
+
const selectMatch = availableViews.find(
|
|
529
|
+
(fv) => String(fv).toLowerCase() === "select"
|
|
530
|
+
);
|
|
531
|
+
if (selectMatch) return selectMatch;
|
|
532
|
+
}
|
|
533
|
+
|
|
234
534
|
// If a specific fieldview was requested by the user, try to honor it
|
|
235
|
-
// but ONLY if it actually exists in available views
|
|
535
|
+
// but ONLY if it actually exists in available views and is appropriate for the mode
|
|
236
536
|
if (requestedFieldview) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
537
|
+
if (
|
|
538
|
+
isShowMode &&
|
|
539
|
+
EDIT_ONLY_FIELDVIEWS.has(String(requestedFieldview).toLowerCase())
|
|
540
|
+
) {
|
|
541
|
+
// Fall through to show-mode defaults — don't use edit fieldviews in show context
|
|
542
|
+
} else {
|
|
543
|
+
const validated = validateAndReturn(requestedFieldview);
|
|
544
|
+
if (validated) return validated;
|
|
545
|
+
// Requested fieldview not available for this field - fall through to defaults
|
|
546
|
+
}
|
|
240
547
|
}
|
|
241
548
|
|
|
242
549
|
// Get the field's configured default fieldview
|
|
@@ -249,21 +556,21 @@ const pickFieldview = (field, mode, requestedFieldview = null) => {
|
|
|
249
556
|
}
|
|
250
557
|
|
|
251
558
|
// Mode-based selection from available fieldviews
|
|
252
|
-
if (mode === "show" || mode === "list") {
|
|
559
|
+
if (mode === "show" || mode === "list" || mode === "listcolumns") {
|
|
253
560
|
// For show mode, prefer simple text-based views, but only from available views
|
|
254
561
|
const showPreferences = ["as_text", "show", "as_string", "text", "showas"];
|
|
255
562
|
for (const pref of showPreferences) {
|
|
256
|
-
const match = availableViews.find(
|
|
257
|
-
String(fv).toLowerCase()
|
|
563
|
+
const match = availableViews.find(
|
|
564
|
+
(fv) => String(fv).toLowerCase() === pref
|
|
258
565
|
);
|
|
259
566
|
if (match) return match;
|
|
260
567
|
}
|
|
261
568
|
} else if (mode === "edit" || mode === "filter") {
|
|
262
|
-
// For edit mode, prefer edit-capable fieldviews from available views
|
|
569
|
+
// For edit mode, prefer edit-capable fieldviews from available views (exact match)
|
|
263
570
|
const editPreferences = ["edit", "input", "select", "textarea"];
|
|
264
571
|
for (const pref of editPreferences) {
|
|
265
|
-
const match = availableViews.find(
|
|
266
|
-
String(fv).toLowerCase()
|
|
572
|
+
const match = availableViews.find(
|
|
573
|
+
(fv) => String(fv).toLowerCase() === pref
|
|
267
574
|
);
|
|
268
575
|
if (match) return match;
|
|
269
576
|
}
|
|
@@ -343,9 +650,12 @@ const normalizeSegment = (segment, ctx) => {
|
|
|
343
650
|
if (typeof segment !== "object") return null;
|
|
344
651
|
|
|
345
652
|
const clone = { ...segment };
|
|
653
|
+
if (typeof clone.style === "string") {
|
|
654
|
+
clone.style = parseStyleString(clone.style);
|
|
655
|
+
}
|
|
346
656
|
if (clone.type === "prompt") return null;
|
|
347
657
|
|
|
348
|
-
if (!clone.type && clone.above) {
|
|
658
|
+
if ((!clone.type || clone.type === "stack") && clone.above) {
|
|
349
659
|
const above = ensureArray(clone.above)
|
|
350
660
|
.map((child) => normalizeSegment(child, ctx))
|
|
351
661
|
.filter(Boolean);
|
|
@@ -353,7 +663,7 @@ const normalizeSegment = (segment, ctx) => {
|
|
|
353
663
|
}
|
|
354
664
|
if (!clone.type && clone.besides) {
|
|
355
665
|
const besides = ensureArray(clone.besides).map((child) =>
|
|
356
|
-
child == null ? null : normalizeSegment(child, ctx)
|
|
666
|
+
child == null ? null : normalizeSegment(child, ctx)
|
|
357
667
|
);
|
|
358
668
|
if (!besides.some((child) => child)) return null;
|
|
359
669
|
return {
|
|
@@ -362,8 +672,38 @@ const normalizeSegment = (segment, ctx) => {
|
|
|
362
672
|
widths: normalizeWidths(clone.widths, besides.length),
|
|
363
673
|
};
|
|
364
674
|
}
|
|
675
|
+
// Handle {columns: {besides: [...], widths: [...]}} wrapper the LLM sometimes emits
|
|
676
|
+
if (!clone.type && clone.columns?.besides) {
|
|
677
|
+
const inner = clone.columns;
|
|
678
|
+
const besides = ensureArray(inner.besides).map((child) =>
|
|
679
|
+
child == null ? null : normalizeSegment(child, ctx)
|
|
680
|
+
);
|
|
681
|
+
if (!besides.some(Boolean)) return null;
|
|
682
|
+
return {
|
|
683
|
+
besides,
|
|
684
|
+
widths: normalizeWidths(inner.widths, besides.length),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
365
687
|
|
|
366
688
|
switch (clone.type) {
|
|
689
|
+
case "columns":
|
|
690
|
+
case "row":
|
|
691
|
+
case "grid": {
|
|
692
|
+
// LLM uses these as a besides wrapper with widths
|
|
693
|
+
if (clone.besides) {
|
|
694
|
+
const besides = ensureArray(clone.besides).map((child) =>
|
|
695
|
+
child == null ? null : normalizeSegment(child, ctx)
|
|
696
|
+
);
|
|
697
|
+
if (!besides.some(Boolean)) return null;
|
|
698
|
+
return {
|
|
699
|
+
besides,
|
|
700
|
+
widths: normalizeWidths(clone.widths, besides.length),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
// fall through to container-like handling if no besides
|
|
704
|
+
const contents = normalizeChild(clone.contents, ctx);
|
|
705
|
+
return contents ? { ...clone, contents } : null;
|
|
706
|
+
}
|
|
367
707
|
case "container": {
|
|
368
708
|
const contents = normalizeChild(clone.contents, ctx);
|
|
369
709
|
return contents
|
|
@@ -388,7 +728,10 @@ const normalizeSegment = (segment, ctx) => {
|
|
|
388
728
|
}
|
|
389
729
|
case "tabs": {
|
|
390
730
|
const tabs = normalizeTabs(clone.tabs, ctx);
|
|
391
|
-
|
|
731
|
+
if (!tabs.length) return null;
|
|
732
|
+
const titles = clone.titles ?? tabs.map((t) => t.title || "");
|
|
733
|
+
const contents = clone.contents ?? tabs.map((t) => t.contents);
|
|
734
|
+
return { ...clone, tabs, titles, contents, class: clone.class || "" };
|
|
392
735
|
}
|
|
393
736
|
case "blank":
|
|
394
737
|
return {
|
|
@@ -428,17 +771,51 @@ const normalizeSegment = (segment, ctx) => {
|
|
|
428
771
|
state: clone.state || {},
|
|
429
772
|
class: clone.class || "",
|
|
430
773
|
};
|
|
431
|
-
case "view_link":
|
|
774
|
+
case "view_link": {
|
|
432
775
|
if (!ctx.viewNames.length) return null;
|
|
776
|
+
const resolvedView = ctx.viewNames.includes(clone.view)
|
|
777
|
+
? clone.view
|
|
778
|
+
: ctx.viewNames[0];
|
|
779
|
+
let relation = clone.relation;
|
|
780
|
+
if (!relation && ctx.table && ctx.schemaData) {
|
|
781
|
+
try {
|
|
782
|
+
const finder = new RelationsFinder(
|
|
783
|
+
ctx.schemaData.tables,
|
|
784
|
+
ctx.schemaData.views,
|
|
785
|
+
6
|
|
786
|
+
);
|
|
787
|
+
const relations = finder.findRelations(
|
|
788
|
+
ctx.table.name,
|
|
789
|
+
resolvedView,
|
|
790
|
+
[]
|
|
791
|
+
);
|
|
792
|
+
if (relations.length > 0) {
|
|
793
|
+
const picked = pickRelation(relations);
|
|
794
|
+
if (picked) relation = picked.relationString;
|
|
795
|
+
}
|
|
796
|
+
} catch (e) {
|
|
797
|
+
console.error("view_link relation lookup failed:", e.message);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// Convert {{ expr }} template syntax to isFormula.label: true with JS expression
|
|
801
|
+
let viewLabel = clone.view_label || clone.view;
|
|
802
|
+
let isFormula = clone.isFormula || {};
|
|
803
|
+
const tmplMatch =
|
|
804
|
+
typeof viewLabel === "string" && viewLabel.match(/^\{\{\s*(.+?)\s*\}\}$/);
|
|
805
|
+
if (tmplMatch) {
|
|
806
|
+
viewLabel = tmplMatch[1];
|
|
807
|
+
isFormula = { ...isFormula, label: true };
|
|
808
|
+
}
|
|
433
809
|
return {
|
|
434
810
|
...clone,
|
|
435
|
-
view:
|
|
436
|
-
|
|
437
|
-
: ctx.viewNames[0],
|
|
438
|
-
view_label: clone.view_label || clone.view,
|
|
811
|
+
view: resolvedView,
|
|
812
|
+
view_label: viewLabel,
|
|
439
813
|
link_style: clone.link_style || "",
|
|
440
814
|
class: clone.class || "",
|
|
815
|
+
isFormula,
|
|
816
|
+
...(relation ? { relation } : {}),
|
|
441
817
|
};
|
|
818
|
+
}
|
|
442
819
|
case "field": {
|
|
443
820
|
if (!ctx.fields.length) return null;
|
|
444
821
|
const fieldMeta = ctx.fieldMap[clone.field_name] || ctx.fields[0];
|
|
@@ -447,7 +824,7 @@ const normalizeSegment = (segment, ctx) => {
|
|
|
447
824
|
const validFieldview = pickFieldview(
|
|
448
825
|
fieldMeta,
|
|
449
826
|
ctx.mode,
|
|
450
|
-
clone.fieldview
|
|
827
|
+
clone.fieldview
|
|
451
828
|
);
|
|
452
829
|
return {
|
|
453
830
|
...clone,
|
|
@@ -457,6 +834,19 @@ const normalizeSegment = (segment, ctx) => {
|
|
|
457
834
|
class: clone.class || "",
|
|
458
835
|
};
|
|
459
836
|
}
|
|
837
|
+
case "list_columns": {
|
|
838
|
+
const besides = ensureArray(clone.besides).map((child) =>
|
|
839
|
+
child == null ? null : normalizeSegment(child, ctx)
|
|
840
|
+
);
|
|
841
|
+
if (!besides.some(Boolean)) return null;
|
|
842
|
+
return { ...clone, besides, list_columns: true };
|
|
843
|
+
}
|
|
844
|
+
case "list_column": {
|
|
845
|
+
const contents = normalizeChild(clone.contents, ctx);
|
|
846
|
+
return contents
|
|
847
|
+
? { ...clone, contents, header_label: clone.header_label || "" }
|
|
848
|
+
: null;
|
|
849
|
+
}
|
|
460
850
|
case "action": {
|
|
461
851
|
if (!ctx.actions.length) return null;
|
|
462
852
|
const actionName = ctx.actions.includes(clone.action_name)
|
|
@@ -508,7 +898,7 @@ const collectSegments = (segment, out = []) => {
|
|
|
508
898
|
if (segment.contents) collectSegments(segment.contents, out);
|
|
509
899
|
if (segment.tabs) {
|
|
510
900
|
ensureArray(segment.tabs).forEach((tab) =>
|
|
511
|
-
collectSegments(tab.contents, out)
|
|
901
|
+
collectSegments(tab.contents, out)
|
|
512
902
|
);
|
|
513
903
|
}
|
|
514
904
|
return out;
|
|
@@ -559,25 +949,48 @@ const normalizeLayoutCandidate = (candidate, ctx) => {
|
|
|
559
949
|
if (!normalized) {
|
|
560
950
|
normalized = convertForeignLayout(candidate, ctx);
|
|
561
951
|
}
|
|
562
|
-
if (!normalized)
|
|
952
|
+
if (!normalized) return null;
|
|
563
953
|
const layout = Array.isArray(normalized) ? { above: normalized } : normalized;
|
|
564
954
|
return sanitizeNoHtmlSegments(layout);
|
|
565
955
|
};
|
|
566
956
|
|
|
957
|
+
const buildFieldMetadata = (fields) => {
|
|
958
|
+
if (!fields || !fields.length) return null;
|
|
959
|
+
const lines = fields
|
|
960
|
+
.filter((f) => !f.primary_key)
|
|
961
|
+
.map((f) => {
|
|
962
|
+
const attrs = Object.entries(f.attributes || {})
|
|
963
|
+
.filter(
|
|
964
|
+
([, v]) => v !== null && v !== undefined && v !== "" && v !== false
|
|
965
|
+
)
|
|
966
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
967
|
+
.join(", ");
|
|
968
|
+
return ` ${f.name} (${f.type}${f.calculated ? ", calculated" : ""}${
|
|
969
|
+
attrs ? ` — attrs: ${attrs}` : ""
|
|
970
|
+
})`;
|
|
971
|
+
});
|
|
972
|
+
return `Table fields available for this layout:\n${lines.join("\n")}`;
|
|
973
|
+
};
|
|
974
|
+
|
|
567
975
|
const buildPromptText = (userPrompt, ctx, schema) => {
|
|
568
976
|
const parts = [
|
|
569
977
|
`You are an expert Saltcorn layout builder assistant. Your task is to generate a layout for mode "${ctx.mode}" that precisely fulfills the user's request.`,
|
|
570
978
|
'CRITICAL: You must return ONLY a single valid JSON object. Do not include introductory text, explanations, markdown formatting (like ```json), or any pseudo-markup. The output must strictly follow this shape: {"layout": <layout-object>}.',
|
|
571
979
|
'The "layout" object MUST conform entirely to the provided JSON Schema. Do not invent properties, types, or structure not defined in the schema.',
|
|
572
980
|
];
|
|
981
|
+
parts.push(ctx.modeGuidance);
|
|
982
|
+
const fieldMeta = buildFieldMetadata(ctx.fields);
|
|
983
|
+
if (fieldMeta) parts.push(fieldMeta);
|
|
573
984
|
parts.push(
|
|
574
|
-
"When a card or container background is requested, set bgType explicitly (None, Color, Image, or Gradient). For Color use bgColor, for Image use bgFileId plus imageLocation (Top, Card, Body) and optionally imageSize (cover, contain, repeat using cover as default) when location is Card or Body, and for Gradient use gradStartColor, gradEndColor, and numeric gradDirection. Use hex color codes when specifying colors."
|
|
985
|
+
"When a card or container background is requested, set bgType explicitly (None, Color, Image, or Gradient). For Color use bgColor, for Image use bgFileId plus imageLocation (Top, Card, Body) and optionally imageSize (cover, contain, repeat using cover as default) when location is Card or Body, and for Gradient use gradStartColor, gradEndColor, and numeric gradDirection. Use hex color codes when specifying colors."
|
|
575
986
|
);
|
|
576
987
|
parts.push(
|
|
577
|
-
`Here is the strict Saltcorn layout JSON schema you MUST follow to construct the layout. Do not deviate from these definitions:\n${JSON.stringify(
|
|
988
|
+
`Here is the strict Saltcorn layout JSON schema you MUST follow to construct the layout. Do not deviate from these definitions:\n${JSON.stringify(
|
|
989
|
+
schema
|
|
990
|
+
)}`
|
|
578
991
|
);
|
|
579
992
|
parts.push(
|
|
580
|
-
`Based on the schema above, process the following user request and generate the layout JSON. Reminder: ONLY output valid JSON starting with { and ending with }, no markdown fences.\nUser request:\n"${userPrompt}"
|
|
993
|
+
`Based on the schema above, process the following user request and generate the layout JSON. Reminder: ONLY output valid JSON starting with { and ending with }, no markdown fences.\nUser request:\n"${userPrompt}"`
|
|
581
994
|
);
|
|
582
995
|
return parts.join("\n\n");
|
|
583
996
|
};
|
|
@@ -796,6 +1209,22 @@ const buildBracketObject = (node) => {
|
|
|
796
1209
|
return obj;
|
|
797
1210
|
};
|
|
798
1211
|
|
|
1212
|
+
const pickRelation = (relations) => {
|
|
1213
|
+
let own = null,
|
|
1214
|
+
parent = null,
|
|
1215
|
+
child = null;
|
|
1216
|
+
for (const r of relations) {
|
|
1217
|
+
if (r.type === RelationType.OWN) own = r;
|
|
1218
|
+
else if (r.type === RelationType.PARENT_SHOW) parent = r;
|
|
1219
|
+
else if (
|
|
1220
|
+
r.type === RelationType.CHILD_LIST ||
|
|
1221
|
+
r.type === RelationType.ONE_TO_ONE_SHOW
|
|
1222
|
+
)
|
|
1223
|
+
child = r;
|
|
1224
|
+
}
|
|
1225
|
+
return own || parent || child || relations[0];
|
|
1226
|
+
};
|
|
1227
|
+
|
|
799
1228
|
const buildContext = async (mode, tableName) => {
|
|
800
1229
|
const normalizedMode = (mode || "show").toLowerCase();
|
|
801
1230
|
const ctx = {
|
|
@@ -806,15 +1235,25 @@ const buildContext = async (mode, tableName) => {
|
|
|
806
1235
|
fieldMap: {},
|
|
807
1236
|
actions: [],
|
|
808
1237
|
viewNames: [],
|
|
1238
|
+
viewTableMap: {},
|
|
1239
|
+
schemaData: null,
|
|
809
1240
|
};
|
|
810
1241
|
|
|
811
1242
|
// Global actions and views are useful even when no table is specified (page builder)
|
|
812
1243
|
const stateActions = Object.keys(getState().actions || {});
|
|
813
1244
|
try {
|
|
814
|
-
const
|
|
815
|
-
ctx.
|
|
1245
|
+
const schemaData = await build_schema_data();
|
|
1246
|
+
ctx.schemaData = schemaData;
|
|
1247
|
+
ctx.viewNames = schemaData.views.map((v) => v.name).filter(Boolean);
|
|
1248
|
+
for (const v of schemaData.views) {
|
|
1249
|
+
if (v.name && v.table_id) {
|
|
1250
|
+
const vt = Table.findOne({ id: v.table_id });
|
|
1251
|
+
if (vt) ctx.viewTableMap[v.name] = vt.name;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
816
1254
|
} catch (err) {
|
|
817
1255
|
ctx.viewNames = [];
|
|
1256
|
+
ctx.viewTableMap = {};
|
|
818
1257
|
}
|
|
819
1258
|
|
|
820
1259
|
if (!tableName) {
|
|
@@ -823,7 +1262,7 @@ const buildContext = async (mode, tableName) => {
|
|
|
823
1262
|
}).filter((tr) => tr.name && !tr.table_id);
|
|
824
1263
|
|
|
825
1264
|
ctx.actions = Array.from(
|
|
826
|
-
new Set([...stateActions, ...triggers.map((tr) => tr.name)])
|
|
1265
|
+
new Set([...stateActions, ...triggers.map((tr) => tr.name)])
|
|
827
1266
|
).filter(Boolean);
|
|
828
1267
|
return ctx;
|
|
829
1268
|
}
|
|
@@ -843,7 +1282,13 @@ const buildContext = async (mode, tableName) => {
|
|
|
843
1282
|
}
|
|
844
1283
|
if (rawFields?.then) rawFields = await rawFields;
|
|
845
1284
|
const fields = (rawFields || []).map((field) => {
|
|
846
|
-
|
|
1285
|
+
let fieldviews = Object.keys(field.type?.fieldviews || {});
|
|
1286
|
+
// FK fields have type "Key" (a string) so field.type?.fieldviews is empty;
|
|
1287
|
+
// ensure select and show are always available for them
|
|
1288
|
+
const typeName = field.type?.name || field.type || "";
|
|
1289
|
+
if (!fieldviews.length && String(typeName).startsWith("Key")) {
|
|
1290
|
+
fieldviews = ["select", "show"];
|
|
1291
|
+
}
|
|
847
1292
|
const isPkName =
|
|
848
1293
|
table.pk_name &&
|
|
849
1294
|
typeof field.name === "string" &&
|
|
@@ -868,6 +1313,7 @@ const buildContext = async (mode, tableName) => {
|
|
|
868
1313
|
is_pk_name: !!isPkName,
|
|
869
1314
|
default_fieldview: defaultFieldview,
|
|
870
1315
|
fieldviews: fieldviews.length ? fieldviews : ["show"],
|
|
1316
|
+
attributes: field.attributes || {},
|
|
871
1317
|
};
|
|
872
1318
|
});
|
|
873
1319
|
|
|
@@ -888,7 +1334,7 @@ const buildContext = async (mode, tableName) => {
|
|
|
888
1334
|
? edit_build_in_actions || []
|
|
889
1335
|
: ["Delete", "GoBack"];
|
|
890
1336
|
const actions = Array.from(
|
|
891
|
-
new Set([...builtIns, ...stateActions, ...triggers.map((tr) => tr.name)])
|
|
1337
|
+
new Set([...builtIns, ...stateActions, ...triggers.map((tr) => tr.name)])
|
|
892
1338
|
).filter(Boolean);
|
|
893
1339
|
|
|
894
1340
|
ctx.table = table;
|
|
@@ -952,8 +1398,8 @@ const buildErrorLayout = ({ message, mode, table }) => {
|
|
|
952
1398
|
};
|
|
953
1399
|
|
|
954
1400
|
module.exports = {
|
|
955
|
-
|
|
956
|
-
|
|
1401
|
+
normalizeLayoutCandidate,
|
|
1402
|
+
run: async (prompt, mode, table, existing_layout, chat) => {
|
|
957
1403
|
prompt = prompt.trim().replace(/^\[\w+\]:\s*/, "");
|
|
958
1404
|
|
|
959
1405
|
const ctx = await buildContext(mode, table);
|
|
@@ -961,28 +1407,46 @@ module.exports = {
|
|
|
961
1407
|
const llm = getState().functions.llm_generate;
|
|
962
1408
|
if (!llm?.run) throw new Error("LLM generator not configured");
|
|
963
1409
|
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
1410
|
+
const llmConfig = await getLlmConfigurationSafe();
|
|
1411
|
+
const allowResponseFormat = canUseResponseFormat(llmConfig);
|
|
1412
|
+
|
|
1413
|
+
let llmPrompt = buildPromptText(prompt, ctx, schema);
|
|
1414
|
+
if (existing_layout !== undefined && existing_layout !== null) {
|
|
1415
|
+
const layoutJson =
|
|
1416
|
+
typeof existing_layout === "string"
|
|
1417
|
+
? existing_layout
|
|
1418
|
+
: JSON.stringify(existing_layout);
|
|
1419
|
+
llmPrompt = `${llmPrompt}\n\nExisting layout (reproduce this with the requested changes applied):\n${layoutJson}`;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
967
1422
|
let responseFormat;
|
|
968
1423
|
try {
|
|
969
1424
|
if (!schema || !schema.schema) {
|
|
970
1425
|
throw new Error("Builder schema unavailable");
|
|
971
1426
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (schemaHasRef(deref)) {
|
|
975
|
-
console.warn(
|
|
976
|
-
"Builder response schema still contains $ref; skipping response_format",
|
|
977
|
-
);
|
|
1427
|
+
if (!allowResponseFormat) {
|
|
1428
|
+
console.warn("LLM backend does not support response_format; skipping");
|
|
978
1429
|
} else {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
1430
|
+
validateSchemaRefs(schema.schema);
|
|
1431
|
+
let simplified = simplifySchemaForLlm(schema.schema);
|
|
1432
|
+
const optionalCount = countOptionalParams(simplified);
|
|
1433
|
+
console.log({ optionalCount });
|
|
1434
|
+
if (optionalCount > 24) {
|
|
1435
|
+
simplified = trimOptionalProperties(simplified, 24);
|
|
1436
|
+
}
|
|
1437
|
+
if (simplified && schemaHasRef(simplified)) {
|
|
1438
|
+
console.warn(
|
|
1439
|
+
"Builder response schema still contains $ref; skipping response_format"
|
|
1440
|
+
);
|
|
1441
|
+
} else if (simplified) {
|
|
1442
|
+
responseFormat = {
|
|
1443
|
+
type: "json_schema",
|
|
1444
|
+
json_schema: {
|
|
1445
|
+
name: "saltcorn_layout",
|
|
1446
|
+
schema: simplified,
|
|
1447
|
+
},
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
986
1450
|
}
|
|
987
1451
|
} catch (err) {
|
|
988
1452
|
console.warn("Builder response schema validation failed", err);
|
|
@@ -1003,13 +1467,12 @@ module.exports = {
|
|
|
1003
1467
|
if (!schema || !schema.schema) {
|
|
1004
1468
|
throw new Error("Builder schema unavailable");
|
|
1005
1469
|
}
|
|
1006
|
-
console.log("¢¢¢¢", { options });
|
|
1007
1470
|
rawResponse = await llm.run(llmPrompt, options);
|
|
1008
|
-
console.log(JSON.stringify(rawResponse, null, 2));
|
|
1009
1471
|
payload = parseJsonPayload(rawResponse);
|
|
1010
|
-
console.log(JSON.stringify({ payload }, null, 2));
|
|
1011
1472
|
const candidate = payload.layout ?? payload;
|
|
1012
|
-
|
|
1473
|
+
const result = normalizeLayoutCandidate(candidate, ctx);
|
|
1474
|
+
if (!result) throw new Error("Empty layout after normalization");
|
|
1475
|
+
return result;
|
|
1013
1476
|
} catch (err) {
|
|
1014
1477
|
let lastError = err;
|
|
1015
1478
|
try {
|
|
@@ -1017,7 +1480,9 @@ module.exports = {
|
|
|
1017
1480
|
rawResponse = await llm.run(repairPrompt, options);
|
|
1018
1481
|
payload = parseJsonPayload(rawResponse);
|
|
1019
1482
|
const candidate = payload.layout ?? payload;
|
|
1020
|
-
|
|
1483
|
+
const result = normalizeLayoutCandidate(candidate, ctx);
|
|
1484
|
+
if (!result) throw new Error("Empty layout after normalization");
|
|
1485
|
+
return result;
|
|
1021
1486
|
} catch (repairErr) {
|
|
1022
1487
|
lastError = repairErr;
|
|
1023
1488
|
}
|
|
@@ -1027,7 +1492,9 @@ module.exports = {
|
|
|
1027
1492
|
rawResponse = await llm.run(reducedPrompt, options);
|
|
1028
1493
|
payload = parseJsonPayload(rawResponse);
|
|
1029
1494
|
const candidate = payload.layout ?? payload;
|
|
1030
|
-
|
|
1495
|
+
const result = normalizeLayoutCandidate(candidate, ctx);
|
|
1496
|
+
if (!result) throw new Error("Empty layout after normalization");
|
|
1497
|
+
return result;
|
|
1031
1498
|
} catch (reducedErr) {
|
|
1032
1499
|
lastError = reducedErr;
|
|
1033
1500
|
}
|
|
@@ -1047,6 +1514,7 @@ module.exports = {
|
|
|
1047
1514
|
{ name: "prompt", type: "String" },
|
|
1048
1515
|
{ name: "mode", type: "String" },
|
|
1049
1516
|
{ name: "table", type: "String" },
|
|
1517
|
+
{ name: "existing_layout", type: "JSON" },
|
|
1050
1518
|
{ name: "chat", type: "JSON" },
|
|
1051
1519
|
],
|
|
1052
1520
|
};
|