@acorex-platform/extension-ai-formbuilder-client 0.0.37 → 0.0.46
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.
|
@@ -1,162 +1,57 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Injectable, NgModule } from '@angular/core';
|
|
3
|
-
import { AXMPlatformManagementModule } from '@acorex-platform/module-platform-management-client';
|
|
2
|
+
import { inject, DestroyRef, Injectable, NgModule } from '@angular/core';
|
|
4
3
|
import { AXP_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_PROVIDER } from '@acorex-platform/framework-client/common';
|
|
5
|
-
import {
|
|
4
|
+
import { AXPPageContextRegistry, AXPPageContextPriority, AXP_HOOK_PROVIDER } from '@acorex-platform/framework-client/core';
|
|
6
5
|
import { provideCommandSetups } from '@acorex-platform/framework-client/runtime';
|
|
6
|
+
import { AXPLayoutDesignerFooterHookKeys, AXPLayoutDesignerHeaderMenuHookKeys } from '@acorex-platform/framework-client/layout/designer';
|
|
7
|
+
import { AXToastService } from '@acorex/components/toast';
|
|
7
8
|
import { AXTranslationService } from '@acorex/core/translation';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { AXMContextualAssistLauncherService } from '@acorex-platform/extension-ai-conversation-client';
|
|
10
|
+
import { AXPEntityService } from '@acorex-platform/framework-client/layout/entity';
|
|
11
|
+
import { coerceUnknownToMultiLanguageString } from '@acorex-platform/framework-shared/core';
|
|
12
|
+
import { axmReadAssistResponseStatus, axmIsAssistStreamingStatus, axmAssistWidgetNodeFromUnknown, AXMConversationRealtimeBus } from '@acorex-platform/module-conversation-client';
|
|
13
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
14
|
+
import { merge } from 'rxjs';
|
|
15
|
+
import { extractMessageTextFromPayload, axmSanitizeContextualAssistValue } from '@acorex-platform/module-conversation-client/contracts';
|
|
10
16
|
import { defineCommand, AXP_COMMAND_DEFINITION_CATEGORY_AI_TOOLS } from '@acorex-platform/framework-shared/runtime';
|
|
11
17
|
|
|
12
|
-
//#region
|
|
13
|
-
/** Entity type for form-template designer
|
|
14
|
-
const AXE_AI_FORMBUILDER_DESIGNER_ENTITY_TYPE = '
|
|
15
|
-
/** Fallback
|
|
18
|
+
//#region ----- Form designer contextual assist -----
|
|
19
|
+
/** Entity type for form-template designer assist (`AssessmentManagement.Template`). */
|
|
20
|
+
const AXE_AI_FORMBUILDER_DESIGNER_ENTITY_TYPE = 'AssessmentManagement.Template';
|
|
21
|
+
/** Fallback record id when the designer has no persisted template id yet. */
|
|
16
22
|
const AXE_AI_FORMBUILDER_DESIGNER_DRAFT_ENTITY_ID = 'draft';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//#region ---- Service ----
|
|
22
|
-
/**
|
|
23
|
-
* Form-template designer AI assist entry point: opens the entity-scoped mini chat panel
|
|
24
|
-
* bound to the current template (`FormTemplateManagement.Template` + template id).
|
|
25
|
-
*
|
|
26
|
-
* Layout generation from chat responses is handled separately in the conversation layer.
|
|
27
|
-
*/
|
|
28
|
-
class AXEAiFormbuilderDesignerAiAssistService {
|
|
29
|
-
constructor() {
|
|
30
|
-
//#region ---- Services & Dependencies ----
|
|
31
|
-
this.miniChatPanel = inject(AXMMiniChatPanelService);
|
|
32
|
-
this.translationService = inject(AXTranslationService);
|
|
33
|
-
}
|
|
34
|
-
//#endregion
|
|
35
|
-
//#region ---- Public API ----
|
|
36
|
-
/**
|
|
37
|
-
* Opens (or toggles) the mini chat panel for the designer's current template entity.
|
|
38
|
-
*/
|
|
39
|
-
async runFromDesigner(designer) {
|
|
40
|
-
const svc = designer;
|
|
41
|
-
const entityId = svc.templateId()?.trim() || AXE_AI_FORMBUILDER_DESIGNER_DRAFT_ENTITY_ID;
|
|
42
|
-
const title = await this.translationService.translateAsync('@form-template-management:designerAi.dialog-title');
|
|
43
|
-
this.miniChatPanel.toggle({
|
|
44
|
-
entityId,
|
|
45
|
-
entityType: AXE_AI_FORMBUILDER_DESIGNER_ENTITY_TYPE,
|
|
46
|
-
title,
|
|
47
|
-
showInfoBar: false,
|
|
48
|
-
showComposer: true,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderDesignerAiAssistService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
52
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderDesignerAiAssistService }); }
|
|
53
|
-
}
|
|
54
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderDesignerAiAssistService, decorators: [{
|
|
55
|
-
type: Injectable
|
|
56
|
-
}] });
|
|
57
|
-
|
|
58
|
-
//#region ---- Formbuilder designer header root menu ----
|
|
59
|
-
/** Registers the layout designer header root-menu entry for AI assist (wand icon). */
|
|
60
|
-
const AXEAiFormbuilderDesignerRootMenuHookProvider = {
|
|
61
|
-
key: AXPLayoutDesignerHeaderMenuHookKeys.root,
|
|
62
|
-
priority: 40,
|
|
63
|
-
execute: (payload) => {
|
|
64
|
-
const assist = inject(AXEAiFormbuilderDesignerAiAssistService);
|
|
65
|
-
payload.items.push({
|
|
66
|
-
id: 'extensions-ai-formbuilder-layout-assist',
|
|
67
|
-
priority: 20,
|
|
68
|
-
labelKey: 'AI Assist',
|
|
69
|
-
iconClass: 'fa-solid fa-wand-magic-sparkles',
|
|
70
|
-
run: () => {
|
|
71
|
-
void assist.runFromDesigner(payload.designer);
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
return payload;
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
//#endregion
|
|
78
|
-
|
|
79
|
-
//#region ---- Imports ----
|
|
80
|
-
//#endregion
|
|
81
|
-
//#region ---- Formbuilder node segment preset ----
|
|
82
|
-
const FORM_BUILDER_NODE_SEGMENT = {
|
|
83
|
-
name: 'node',
|
|
84
|
-
title: '@ai-management:agents.entities.agent.fields.output-contract.transcript-segment.options.node',
|
|
85
|
-
applicableKinds: ['object'],
|
|
86
|
-
contract: {
|
|
87
|
-
kind: 'object',
|
|
88
|
-
transcriptSegment: 'node',
|
|
89
|
-
validationCommandName: 'AiFormbuilder:ValidateLayoutJsonForAi',
|
|
90
|
-
structuredRetries: 3,
|
|
91
|
-
schemaSummary: 'Chat UI: block-layout or table-layout root with mode edit/view. Form Template designer: document-layout → page-layout → fieldset/flex/grid. No raw CSS keys.',
|
|
92
|
-
},
|
|
93
|
-
};
|
|
23
|
+
/** Launch kind stored on the assist room (generic contextual-assist bag). */
|
|
24
|
+
const AXE_AI_FORMBUILDER_ASSIST_KIND = 'form-template-designer';
|
|
25
|
+
/** Action name passed into {@link AXMContextualAssistLauncherService}. */
|
|
26
|
+
const AXE_AI_FORMBUILDER_ASSIST_ACTION = 'form.template.designer.assist';
|
|
94
27
|
/**
|
|
95
|
-
*
|
|
28
|
+
* Launch-context scenario for Form Template designer rooms
|
|
29
|
+
* (hide chat widget trees; page-context canvas edits).
|
|
96
30
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
31
|
+
const AXE_AI_FORMBUILDER_ASSIST_SCENARIO = 'form-template-designer';
|
|
32
|
+
const AXE_AI_FORMBUILDER_VALIDATE_LAYOUT_COMMAND = 'AiFormbuilder:ValidateLayoutJsonForAi';
|
|
33
|
+
const AXE_AI_FORMBUILDER_APPLY_LAYOUT_COMMAND = 'AiFormbuilder:ApplyLayoutJsonForAi';
|
|
34
|
+
const AXE_AI_FORMBUILDER_OUTLINE_LAYOUT_COMMAND = 'AiFormbuilder:OutlineLayoutForAi';
|
|
35
|
+
const AXE_AI_FORMBUILDER_ADD_WIDGET_COMMAND = 'AiFormbuilder:AddWidgetForAi';
|
|
36
|
+
const AXE_AI_FORMBUILDER_UPDATE_WIDGET_COMMAND = 'AiFormbuilder:UpdateWidgetForAi';
|
|
37
|
+
const AXE_AI_FORMBUILDER_REMOVE_WIDGET_COMMAND = 'AiFormbuilder:RemoveWidgetForAi';
|
|
38
|
+
const AXE_AI_FORMBUILDER_MOVE_WIDGET_COMMAND = 'AiFormbuilder:MoveWidgetForAi';
|
|
39
|
+
/** Page-context registration id while the form designer is the focused page. */
|
|
40
|
+
const AXE_AI_FORMBUILDER_PAGE_CONTEXT_ID = 'layout-designer';
|
|
41
|
+
/** Advertised Page:RunAction id that writes the open designer canvas. */
|
|
42
|
+
const AXE_AI_FORMBUILDER_PAGE_ACTION_APPLY_LAYOUT = 'apply-layout';
|
|
43
|
+
const AXE_AI_FORMBUILDER_PAGE_ACTION_ADD_WIDGET = 'add-widget';
|
|
44
|
+
const AXE_AI_FORMBUILDER_PAGE_ACTION_UPDATE_WIDGET = 'update-widget';
|
|
45
|
+
const AXE_AI_FORMBUILDER_PAGE_ACTION_REMOVE_WIDGET = 'remove-widget';
|
|
46
|
+
const AXE_AI_FORMBUILDER_PAGE_ACTION_MOVE_WIDGET = 'move-widget';
|
|
47
|
+
/** Seeded Form Designer assist (`AiManagement.Assist.name` = form-designer-assistant). Must match `AXMAiFormDesignerAssistId`. */
|
|
48
|
+
const AXE_AI_FORMBUILDER_DESIGNER_ASSIST_ID = 'b0000001-0000-4000-8000-000000000004';
|
|
105
49
|
//#endregion
|
|
106
|
-
//#region ---- Module ----
|
|
107
|
-
class AXEAIFormBuilderExtensionModule {
|
|
108
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIFormBuilderExtensionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
109
|
-
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXEAIFormBuilderExtensionModule, imports: [AXMPlatformManagementModule] }); }
|
|
110
|
-
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIFormBuilderExtensionModule, providers: [
|
|
111
|
-
AXEAiFormbuilderDesignerAiAssistService,
|
|
112
|
-
provideCommandSetups([
|
|
113
|
-
{
|
|
114
|
-
key: 'AiFormbuilder:ValidateLayoutJsonForAi',
|
|
115
|
-
command: () => Promise.resolve().then(function () { return validateLayoutJsonForAi_command; }).then((c) => c.AXEAiFormbuilderValidateLayoutJsonForAiCommand),
|
|
116
|
-
},
|
|
117
|
-
]),
|
|
118
|
-
{
|
|
119
|
-
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
120
|
-
multi: true,
|
|
121
|
-
useValue: AXEAiFormbuilderDesignerRootMenuHookProvider,
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
provide: AXP_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_PROVIDER,
|
|
125
|
-
useClass: AXPAiFormbuilderOutputContractTranscriptSegmentProvider,
|
|
126
|
-
multi: true,
|
|
127
|
-
},
|
|
128
|
-
], imports: [AXMPlatformManagementModule] }); }
|
|
129
|
-
}
|
|
130
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIFormBuilderExtensionModule, decorators: [{
|
|
131
|
-
type: NgModule,
|
|
132
|
-
args: [{
|
|
133
|
-
imports: [AXMPlatformManagementModule],
|
|
134
|
-
providers: [
|
|
135
|
-
AXEAiFormbuilderDesignerAiAssistService,
|
|
136
|
-
provideCommandSetups([
|
|
137
|
-
{
|
|
138
|
-
key: 'AiFormbuilder:ValidateLayoutJsonForAi',
|
|
139
|
-
command: () => Promise.resolve().then(function () { return validateLayoutJsonForAi_command; }).then((c) => c.AXEAiFormbuilderValidateLayoutJsonForAiCommand),
|
|
140
|
-
},
|
|
141
|
-
]),
|
|
142
|
-
{
|
|
143
|
-
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
144
|
-
multi: true,
|
|
145
|
-
useValue: AXEAiFormbuilderDesignerRootMenuHookProvider,
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
provide: AXP_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_PROVIDER,
|
|
149
|
-
useClass: AXPAiFormbuilderOutputContractTranscriptSegmentProvider,
|
|
150
|
-
multi: true,
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
}]
|
|
154
|
-
}] });
|
|
155
50
|
|
|
51
|
+
//#region ----- Imports -----
|
|
156
52
|
//#endregion
|
|
157
|
-
//#region
|
|
53
|
+
//#region ----- Constants -----
|
|
158
54
|
const MAX_ERRORS = 48;
|
|
159
|
-
/** Keys that must not appear on widget nodes — styling belongs in widget options, not DOM/CSS. */
|
|
160
55
|
const FORBIDDEN_LAYOUT_KEYS = new Set([
|
|
161
56
|
'style',
|
|
162
57
|
'styles',
|
|
@@ -167,7 +62,64 @@ const FORBIDDEN_LAYOUT_KEYS = new Set([
|
|
|
167
62
|
'innerHTML',
|
|
168
63
|
'dataset',
|
|
169
64
|
]);
|
|
170
|
-
|
|
65
|
+
/** Roots the form-template designer can load (File → Download / Import JSON). */
|
|
66
|
+
const DESIGNER_CANVAS_ROOT_TYPES = new Set(['document-layout', 'page-layout']);
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region ----- Parse & validate -----
|
|
69
|
+
/**
|
|
70
|
+
* AI tool-arg normalization may pass `json` as a string or an already-parsed object.
|
|
71
|
+
* Also accepts `layoutJson` from older registry rows.
|
|
72
|
+
*/
|
|
73
|
+
function axeResolveLayoutJsonInput(input) {
|
|
74
|
+
return resolveLayoutJsonString(input?.json) || resolveLayoutJsonString(input?.layoutJson);
|
|
75
|
+
}
|
|
76
|
+
function axeParseAndValidateLayoutJson(json) {
|
|
77
|
+
const raw = resolveLayoutJsonString(json);
|
|
78
|
+
if (!raw) {
|
|
79
|
+
return { valid: false, errors: ['Missing json string.'] };
|
|
80
|
+
}
|
|
81
|
+
const stripped = stripJsonFences$1(raw);
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(stripped);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
const msg = e instanceof Error ? e.message : 'Invalid JSON';
|
|
88
|
+
return { valid: false, errors: [`JSON parse error: ${msg}`] };
|
|
89
|
+
}
|
|
90
|
+
const errors = [];
|
|
91
|
+
validateWidgetNodeShape(parsed, '$', errors);
|
|
92
|
+
if (errors.length > 0) {
|
|
93
|
+
return { valid: false, errors };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
valid: true,
|
|
97
|
+
errors: [],
|
|
98
|
+
normalizedJson: JSON.stringify(parsed),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function axeIsDesignerCanvasRoot(value) {
|
|
102
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const type = value['type'];
|
|
106
|
+
return typeof type === 'string' && DESIGNER_CANVAS_ROOT_TYPES.has(type.trim());
|
|
107
|
+
}
|
|
108
|
+
function resolveLayoutJsonString(json) {
|
|
109
|
+
if (typeof json === 'string') {
|
|
110
|
+
return json.trim();
|
|
111
|
+
}
|
|
112
|
+
if (json !== null && json !== undefined && typeof json === 'object') {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.stringify(json);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
function stripJsonFences$1(raw) {
|
|
171
123
|
const t = raw.trim();
|
|
172
124
|
const fence = /^```(?:json)?\s*\r?\n?([\s\S]*?)\r?\n?```\s*$/i.exec(t);
|
|
173
125
|
return fence ? fence[1].trim() : t;
|
|
@@ -213,105 +165,1933 @@ function validateWidgetNodeShape(value, jsonPath, errors) {
|
|
|
213
165
|
}
|
|
214
166
|
}
|
|
215
167
|
//#endregion
|
|
216
|
-
|
|
168
|
+
|
|
169
|
+
//#region ----- Imports -----
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region ----- Constants -----
|
|
172
|
+
const DESIGNER_ROOT_TYPE_MARKER = /"type"\s*:\s*"(document-layout|page-layout)"/i;
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region ----- Message extract -----
|
|
175
|
+
/** True when this assist reply is finished and may be applied to the canvas. */
|
|
176
|
+
function axeAssistMessageReadyToApply(message) {
|
|
177
|
+
const status = axmReadAssistResponseStatus(message.metadata);
|
|
178
|
+
if (axmIsAssistStreamingStatus(status) || status === 'failed') {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const meta = message.metadata;
|
|
182
|
+
if (meta && meta['isAssistResponse'] === false) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
217
187
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
188
|
+
* Designer JSON from a completed assist message.
|
|
189
|
+
* Accepts a node payload or JSON pasted inside a text reply (the usual specialist failure mode).
|
|
220
190
|
*/
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
191
|
+
function axeDesignerLayoutJsonFromAssistMessage(message) {
|
|
192
|
+
const payload = message.payload;
|
|
193
|
+
const candidates = [payload];
|
|
194
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
195
|
+
const row = payload;
|
|
196
|
+
if ('content' in row) {
|
|
197
|
+
candidates.push(row['content']);
|
|
198
|
+
}
|
|
224
199
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
200
|
+
for (const text of collectMessageTexts(message)) {
|
|
201
|
+
const extracted = axeExtractDesignerLayoutFromText(text);
|
|
202
|
+
if (extracted !== undefined) {
|
|
203
|
+
candidates.push(extracted);
|
|
228
204
|
}
|
|
229
|
-
|
|
230
|
-
|
|
205
|
+
}
|
|
206
|
+
for (const candidate of candidates) {
|
|
207
|
+
const node = axmAssistWidgetNodeFromUnknown(candidate);
|
|
208
|
+
if (node && axeIsDesignerCanvasRoot(node)) {
|
|
209
|
+
return JSON.stringify(node);
|
|
231
210
|
}
|
|
232
211
|
}
|
|
233
|
-
return
|
|
212
|
+
return null;
|
|
234
213
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
214
|
+
/** Pulls a document-layout / page-layout object out of mixed prose + JSON. */
|
|
215
|
+
function axeExtractDesignerLayoutFromText(raw) {
|
|
216
|
+
const text = stripJsonFences(raw);
|
|
217
|
+
if (!text) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(text);
|
|
222
|
+
const node = axmAssistWidgetNodeFromUnknown(parsed);
|
|
223
|
+
if (node && axeIsDesignerCanvasRoot(node)) {
|
|
224
|
+
return node;
|
|
245
225
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Mixed chat text — scan for an embedded designer root.
|
|
229
|
+
}
|
|
230
|
+
return extractEmbeddedDesignerObject(text);
|
|
231
|
+
}
|
|
232
|
+
function collectMessageTexts(message) {
|
|
233
|
+
const texts = [];
|
|
234
|
+
const fromPayload = extractMessageTextFromPayload(message.payload);
|
|
235
|
+
if (fromPayload.trim()) {
|
|
236
|
+
texts.push(fromPayload);
|
|
237
|
+
}
|
|
238
|
+
const rawPayload = message.payload;
|
|
239
|
+
if (typeof rawPayload === 'string' && rawPayload.trim()) {
|
|
240
|
+
texts.push(rawPayload);
|
|
241
|
+
}
|
|
242
|
+
const meta = message.metadata;
|
|
243
|
+
if (typeof meta?.['text'] === 'string' && meta['text'].trim()) {
|
|
244
|
+
texts.push(meta['text']);
|
|
245
|
+
}
|
|
246
|
+
return texts;
|
|
247
|
+
}
|
|
248
|
+
function stripJsonFences(raw) {
|
|
249
|
+
const t = raw.trim();
|
|
250
|
+
const fence = /^```(?:json)?\s*\r?\n?([\s\S]*?)\r?\n?```\s*$/i.exec(t);
|
|
251
|
+
return fence ? fence[1].trim() : t;
|
|
252
|
+
}
|
|
253
|
+
function extractEmbeddedDesignerObject(text) {
|
|
254
|
+
let searchFrom = 0;
|
|
255
|
+
while (searchFrom < text.length) {
|
|
256
|
+
const slice = text.slice(searchFrom);
|
|
257
|
+
const match = DESIGNER_ROOT_TYPE_MARKER.exec(slice);
|
|
258
|
+
if (!match || match.index < 0) {
|
|
259
|
+
break;
|
|
259
260
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
data: { valid: false, errors },
|
|
266
|
-
};
|
|
261
|
+
const abs = searchFrom + match.index;
|
|
262
|
+
const start = text.lastIndexOf('{', abs);
|
|
263
|
+
if (start < 0) {
|
|
264
|
+
searchFrom = abs + match[0].length;
|
|
265
|
+
continue;
|
|
267
266
|
}
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
267
|
+
const json = sliceBalancedObject(text, start);
|
|
268
|
+
if (json) {
|
|
269
|
+
try {
|
|
270
|
+
const parsed = JSON.parse(json);
|
|
271
|
+
const node = axmAssistWidgetNodeFromUnknown(parsed);
|
|
272
|
+
if (node && axeIsDesignerCanvasRoot(node)) {
|
|
273
|
+
return node;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Try the next type marker.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
searchFrom = abs + match[0].length;
|
|
276
281
|
}
|
|
277
|
-
|
|
278
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderValidateLayoutJsonForAiCommand, providedIn: 'root' }); }
|
|
282
|
+
return undefined;
|
|
279
283
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
284
|
+
function sliceBalancedObject(text, start) {
|
|
285
|
+
if (text[start] !== '{') {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
let depth = 0;
|
|
289
|
+
let inString = false;
|
|
290
|
+
let escape = false;
|
|
291
|
+
for (let i = start; i < text.length; i++) {
|
|
292
|
+
const ch = text[i];
|
|
293
|
+
if (inString) {
|
|
294
|
+
if (escape) {
|
|
295
|
+
escape = false;
|
|
296
|
+
}
|
|
297
|
+
else if (ch === '\\') {
|
|
298
|
+
escape = true;
|
|
299
|
+
}
|
|
300
|
+
else if (ch === '"') {
|
|
301
|
+
inString = false;
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (ch === '"') {
|
|
306
|
+
inString = true;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (ch === '{') {
|
|
310
|
+
depth += 1;
|
|
311
|
+
}
|
|
312
|
+
else if (ch === '}') {
|
|
313
|
+
depth -= 1;
|
|
314
|
+
if (depth === 0) {
|
|
315
|
+
return text.slice(start, i + 1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
289
322
|
|
|
323
|
+
//#region ----- Imports -----
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region ----- Constants -----
|
|
326
|
+
const TEMPLATE_CONTEXT_FIELDS = ['code', 'title', 'description', 'type', 'categories'];
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region ----- Payload -----
|
|
290
329
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
330
|
+
* Snapshot of the live designer chrome for contextual assist.
|
|
331
|
+
* Full layout JSON is not shipped — the specialist outlines and patches the canvas.
|
|
293
332
|
*/
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
333
|
+
function axeBuildFormbuilderDesignerAssistPayload(designer, args) {
|
|
334
|
+
const [moduleName = 'AssessmentManagement', entityName = 'Template'] = AXE_AI_FORMBUILDER_DESIGNER_ENTITY_TYPE.split('.');
|
|
335
|
+
const document = designer.document();
|
|
336
|
+
const currentPage = designer.currentPage();
|
|
337
|
+
const selected = designer.selectedNode();
|
|
338
|
+
const profile = designer.surfaceProfile();
|
|
339
|
+
const pages = Array.isArray(document?.children) ? document.children : [];
|
|
340
|
+
const payload = {
|
|
341
|
+
moduleName,
|
|
342
|
+
entityName,
|
|
343
|
+
entityTitle: args.entityTitle,
|
|
344
|
+
recordId: args.recordId,
|
|
345
|
+
columns: args.columns,
|
|
346
|
+
scenario: AXE_AI_FORMBUILDER_ASSIST_SCENARIO,
|
|
347
|
+
entityType: AXE_AI_FORMBUILDER_DESIGNER_ENTITY_TYPE,
|
|
348
|
+
surface: 'layout-designer',
|
|
349
|
+
unsavedChanges: designer.unsavedChanges(),
|
|
350
|
+
pageCount: pages.length,
|
|
351
|
+
currentPageIndex: designer.currentPageIndex(),
|
|
352
|
+
currentPage: widgetRef(currentPage, true),
|
|
353
|
+
selectedWidget: selected ? widgetRef(selected, false) : null,
|
|
354
|
+
dataSources: summarizeDataSources(designer),
|
|
355
|
+
widgetCounts: countWidgetTypes(document),
|
|
356
|
+
...(profile.id ? { surfaceId: profile.id } : {}),
|
|
357
|
+
...(profile.documentKind ? { documentKind: profile.documentKind } : {}),
|
|
358
|
+
...(profile.rootWidget ? { rootWidget: profile.rootWidget } : {}),
|
|
359
|
+
};
|
|
360
|
+
return payload;
|
|
361
|
+
}
|
|
362
|
+
/** Template record fields for assist (skips the persisted `template` JSON blob). */
|
|
363
|
+
function axeBuildFormTemplateRecordAssistColumns(row, resolveText) {
|
|
364
|
+
const columns = [];
|
|
365
|
+
for (const name of TEMPLATE_CONTEXT_FIELDS) {
|
|
366
|
+
if (!(name in row) || row[name] == null) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const raw = row[name];
|
|
370
|
+
const text = resolveText(raw);
|
|
371
|
+
columns.push({
|
|
372
|
+
name,
|
|
373
|
+
title: name,
|
|
374
|
+
value: axmSanitizeContextualAssistValue(text || raw),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return columns;
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region ----- Designer snapshot -----
|
|
381
|
+
function widgetRef(node, includeChildCount) {
|
|
382
|
+
const type = typeof node?.type === 'string' ? node.type.trim() : '';
|
|
383
|
+
const name = typeof node?.name === 'string' ? node.name.trim() : '';
|
|
384
|
+
const path = typeof node?.path === 'string' ? node.path.trim() : '';
|
|
385
|
+
const childCount = Array.isArray(node?.children) ? node.children.length : 0;
|
|
386
|
+
return {
|
|
387
|
+
...(type ? { type } : {}),
|
|
388
|
+
...(name ? { name } : {}),
|
|
389
|
+
...(path ? { path } : {}),
|
|
390
|
+
...(includeChildCount ? { childCount } : {}),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function summarizeDataSources(designer) {
|
|
394
|
+
return (designer.sources() ?? []).slice(0, 12).map((source) => ({
|
|
395
|
+
id: source.id,
|
|
396
|
+
name: source.name,
|
|
397
|
+
itemCount: Array.isArray(source.items) ? source.items.length : 0,
|
|
398
|
+
columnNames: (source.columns ?? []).map((column) => column.name).filter(Boolean).slice(0, 12),
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
function countWidgetTypes(node) {
|
|
402
|
+
const counts = {};
|
|
403
|
+
walkWidgetTypes(node, counts);
|
|
404
|
+
return counts;
|
|
405
|
+
}
|
|
406
|
+
function walkWidgetTypes(node, counts) {
|
|
407
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const row = node;
|
|
411
|
+
const type = typeof row['type'] === 'string' ? row['type'].trim() : '';
|
|
412
|
+
if (type) {
|
|
413
|
+
counts[type] = (counts[type] ?? 0) + 1;
|
|
414
|
+
}
|
|
415
|
+
const children = row['children'];
|
|
416
|
+
if (Array.isArray(children)) {
|
|
417
|
+
for (const child of children) {
|
|
418
|
+
walkWidgetTypes(child, counts);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
//#endregion
|
|
423
|
+
|
|
424
|
+
//#region ----- Imports -----
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region ----- Outline -----
|
|
427
|
+
function axeBuildDesignerOutline(root) {
|
|
428
|
+
const items = [];
|
|
429
|
+
walkOutline(root, null, [], items);
|
|
430
|
+
return items;
|
|
431
|
+
}
|
|
432
|
+
function axeFindDesignerWidget(root, ref) {
|
|
433
|
+
const name = typeof ref.name === 'string' ? ref.name.trim() : '';
|
|
434
|
+
const path = typeof ref.path === 'string' ? ref.path.trim() : '';
|
|
435
|
+
if (!name && !path) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
let named = null;
|
|
439
|
+
let byPath = null;
|
|
440
|
+
walkFind(root, null, [], (hit) => {
|
|
441
|
+
if (name && hit.node.name?.trim() === name && !named) {
|
|
442
|
+
named = hit;
|
|
443
|
+
}
|
|
444
|
+
if (path && hit.path === path) {
|
|
445
|
+
byPath = hit;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
return named ?? byPath;
|
|
449
|
+
}
|
|
450
|
+
function axeResolveWidgetNodeInput(raw) {
|
|
451
|
+
const value = unwrapJson(raw);
|
|
452
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
const type = value['type'];
|
|
456
|
+
if (typeof type !== 'string' || !type.trim()) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
return value;
|
|
460
|
+
}
|
|
461
|
+
function axeReadOptionalIndex(raw) {
|
|
462
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
463
|
+
return Math.trunc(raw);
|
|
464
|
+
}
|
|
465
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
466
|
+
const parsed = Number(raw);
|
|
467
|
+
if (Number.isFinite(parsed)) {
|
|
468
|
+
return Math.trunc(parsed);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
function axeReadStringArg(raw) {
|
|
474
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
475
|
+
}
|
|
476
|
+
/** `set: false` when the caller omitted the arg. Empty array clears triggers. */
|
|
477
|
+
function axeReadOptionalTriggers(raw) {
|
|
478
|
+
if (raw === undefined) {
|
|
479
|
+
return { set: false };
|
|
480
|
+
}
|
|
481
|
+
const value = unwrapJson(raw);
|
|
482
|
+
if (value == null) {
|
|
483
|
+
return { set: true, value: [] };
|
|
484
|
+
}
|
|
485
|
+
if (!Array.isArray(value)) {
|
|
486
|
+
return { set: false };
|
|
487
|
+
}
|
|
488
|
+
const triggers = [];
|
|
489
|
+
for (const item of value) {
|
|
490
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const row = item;
|
|
494
|
+
const event = typeof row['event'] === 'string' ? row['event'].trim() : '';
|
|
495
|
+
const actionRaw = row['action'];
|
|
496
|
+
const action = Array.isArray(actionRaw)
|
|
497
|
+
? actionRaw.filter((part) => typeof part === 'string' && !!part.trim())
|
|
498
|
+
: typeof actionRaw === 'string'
|
|
499
|
+
? actionRaw.trim()
|
|
500
|
+
: '';
|
|
501
|
+
if (!event && (Array.isArray(action) ? action.length === 0 : !action)) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
triggers.push({
|
|
505
|
+
event,
|
|
506
|
+
action,
|
|
507
|
+
...(row['disabled'] === true ? { disabled: true } : {}),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return { set: true, value: triggers };
|
|
511
|
+
}
|
|
512
|
+
function axeReadOptionalVisible(raw) {
|
|
513
|
+
if (raw === undefined) {
|
|
514
|
+
return { set: false };
|
|
515
|
+
}
|
|
516
|
+
if (typeof raw === 'boolean') {
|
|
517
|
+
return { set: true, value: raw };
|
|
518
|
+
}
|
|
519
|
+
if (typeof raw === 'string') {
|
|
520
|
+
return { set: true, value: raw.trim() };
|
|
521
|
+
}
|
|
522
|
+
return { set: false };
|
|
523
|
+
}
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region ----- Walk -----
|
|
526
|
+
function pathSegment(node, siblingIndex) {
|
|
527
|
+
const name = typeof node.name === 'string' ? node.name.trim() : '';
|
|
528
|
+
if (name) {
|
|
529
|
+
return name;
|
|
530
|
+
}
|
|
531
|
+
const type = typeof node.type === 'string' ? node.type.trim() : 'widget';
|
|
532
|
+
return `${type}[${siblingIndex}]`;
|
|
533
|
+
}
|
|
534
|
+
function walkOutline(value, parentName, pathParts, items, siblingIndex = 0) {
|
|
535
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const node = value;
|
|
539
|
+
const type = typeof node.type === 'string' ? node.type.trim() : '';
|
|
540
|
+
if (!type) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const name = typeof node.name === 'string' ? node.name.trim() : '';
|
|
544
|
+
const segment = pathSegment(node, siblingIndex);
|
|
545
|
+
const path = [...pathParts, segment].join('/');
|
|
546
|
+
const label = readLabel(node);
|
|
547
|
+
items.push({
|
|
548
|
+
type,
|
|
549
|
+
path,
|
|
550
|
+
name,
|
|
551
|
+
...(parentName ? { parentName } : {}),
|
|
552
|
+
...(label ? { label } : {}),
|
|
553
|
+
});
|
|
554
|
+
const children = node.children;
|
|
555
|
+
if (!Array.isArray(children)) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
children.forEach((child, index) => {
|
|
559
|
+
walkOutline(child, name || null, [...pathParts, segment], items, index);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function walkFind(value, parent, pathParts, visit, siblingIndex = 0) {
|
|
563
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const node = value;
|
|
567
|
+
const type = typeof node.type === 'string' ? node.type.trim() : '';
|
|
568
|
+
if (!type) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const segment = pathSegment(node, siblingIndex);
|
|
572
|
+
const path = [...pathParts, segment].join('/');
|
|
573
|
+
visit({ node, parent, path });
|
|
574
|
+
const children = node.children;
|
|
575
|
+
if (!Array.isArray(children)) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
children.forEach((child, index) => {
|
|
579
|
+
walkFind(child, node, [...pathParts, segment], visit, index);
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function readLabel(node) {
|
|
583
|
+
const options = node.options;
|
|
584
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
const label = options['label'];
|
|
588
|
+
if (typeof label === 'string' && label.trim()) {
|
|
589
|
+
return label.trim();
|
|
590
|
+
}
|
|
591
|
+
const title = options['title'];
|
|
592
|
+
if (typeof title === 'string' && title.trim()) {
|
|
593
|
+
return title.trim();
|
|
594
|
+
}
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
function unwrapJson(raw) {
|
|
598
|
+
if (typeof raw !== 'string') {
|
|
599
|
+
return raw;
|
|
600
|
+
}
|
|
601
|
+
const trimmed = raw.trim();
|
|
602
|
+
if (!trimmed) {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
return JSON.parse(trimmed);
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
//#endregion
|
|
613
|
+
|
|
614
|
+
//#region ----- Imports -----
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region ----- Actions -----
|
|
617
|
+
const AXE_AI_FORMBUILDER_PAGE_CONTEXT_ACTIONS = [
|
|
618
|
+
{
|
|
619
|
+
id: AXE_AI_FORMBUILDER_PAGE_ACTION_ADD_WIDGET,
|
|
620
|
+
title: 'Add one widget under a named parent on the live canvas',
|
|
621
|
+
params: [
|
|
622
|
+
{
|
|
623
|
+
name: 'parentName',
|
|
624
|
+
required: false,
|
|
625
|
+
description: 'Parent widget name from snapshot.outline. Prefer this over parentPath.',
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: 'parentPath',
|
|
629
|
+
required: false,
|
|
630
|
+
description: 'Parent path from snapshot.outline when the parent has no name.',
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: 'widget',
|
|
634
|
+
required: true,
|
|
635
|
+
description: 'One AXPWidgetNode (type plus name/options/children). Put triggers and visible on this node root when the field calculates or shows/hides. Not the full document-layout.',
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: 'index',
|
|
639
|
+
required: false,
|
|
640
|
+
description: 'Optional child index under the parent. Omit to append.',
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: AXE_AI_FORMBUILDER_PAGE_ACTION_UPDATE_WIDGET,
|
|
646
|
+
title: 'Patch options, validations, triggers, or visible on one canvas widget',
|
|
647
|
+
params: [
|
|
648
|
+
{ name: 'name', required: false, description: 'Widget name from snapshot.outline.' },
|
|
649
|
+
{ name: 'path', required: false, description: 'Widget path from snapshot.outline when it has no name.' },
|
|
650
|
+
{ name: 'options', required: false, description: 'Partial options object merged onto the widget.' },
|
|
651
|
+
{ name: 'validations', required: false, description: 'Replaces options.validations (e.g. [{ "rule": "required" }]).' },
|
|
652
|
+
{
|
|
653
|
+
name: 'triggers',
|
|
654
|
+
required: false,
|
|
655
|
+
description: 'Replaces node.triggers (not options.triggers). Array of { event, action } expression strings. Empty array clears. See write-platform-expression.',
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: 'visible',
|
|
659
|
+
required: false,
|
|
660
|
+
description: 'Replaces node.visible. Boolean or "{{ context.eval(...) }}" expression. See write-platform-expression.',
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
id: AXE_AI_FORMBUILDER_PAGE_ACTION_REMOVE_WIDGET,
|
|
666
|
+
title: 'Remove one widget from the live canvas',
|
|
667
|
+
params: [
|
|
668
|
+
{ name: 'name', required: false, description: 'Widget name from snapshot.outline.' },
|
|
669
|
+
{ name: 'path', required: false, description: 'Widget path from snapshot.outline when it has no name.' },
|
|
670
|
+
],
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
id: AXE_AI_FORMBUILDER_PAGE_ACTION_MOVE_WIDGET,
|
|
674
|
+
title: 'Move one widget to another parent or index',
|
|
675
|
+
params: [
|
|
676
|
+
{ name: 'name', required: false, description: 'Widget name from snapshot.outline.' },
|
|
677
|
+
{ name: 'path', required: false, description: 'Widget path from snapshot.outline when it has no name.' },
|
|
678
|
+
{ name: 'parentName', required: false, description: 'Destination parent name. Omit to reorder under the current parent.' },
|
|
679
|
+
{ name: 'parentPath', required: false, description: 'Destination parent path when the parent has no name.' },
|
|
680
|
+
{ name: 'index', required: false, description: 'Child index under the destination parent.' },
|
|
681
|
+
],
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
id: AXE_AI_FORMBUILDER_PAGE_ACTION_APPLY_LAYOUT,
|
|
685
|
+
title: 'Replace the whole canvas with a complete designer tree',
|
|
686
|
+
params: [
|
|
687
|
+
{
|
|
688
|
+
name: 'json',
|
|
689
|
+
required: true,
|
|
690
|
+
description: 'Complete document-layout or page-layout. Use only for a brand-new form or a large restructure.',
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: 'replaceAll',
|
|
694
|
+
required: false,
|
|
695
|
+
description: 'Set true only when the user asked for a brand-new form. Otherwise a much smaller tree is rejected.',
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
},
|
|
699
|
+
];
|
|
700
|
+
//#endregion
|
|
701
|
+
//#region ----- Snapshot -----
|
|
702
|
+
function axeBuildFormbuilderDesignerPageContextSnapshot(args) {
|
|
703
|
+
const title = args.title.trim() || 'Layout designer';
|
|
704
|
+
return {
|
|
705
|
+
kind: 'layout-designer',
|
|
706
|
+
title,
|
|
707
|
+
status: 'ready',
|
|
708
|
+
entity: {
|
|
709
|
+
fullName: AXE_AI_FORMBUILDER_DESIGNER_ENTITY_TYPE,
|
|
710
|
+
title,
|
|
711
|
+
},
|
|
712
|
+
...(args.outline?.length ? { outline: args.outline } : {}),
|
|
713
|
+
actions: AXE_AI_FORMBUILDER_PAGE_CONTEXT_ACTIONS,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function axeResolvePageContextLayoutJson(args) {
|
|
717
|
+
return axeResolveLayoutJsonInput(args);
|
|
718
|
+
}
|
|
719
|
+
function axeReadReplaceAllFlag(args) {
|
|
720
|
+
const raw = args['replaceAll'];
|
|
721
|
+
return raw === true || raw === 'true';
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Blocks apply-layout from wiping a rich canvas with a flattened/partial tree
|
|
725
|
+
* (the usual supervisor failure: rewrite the form as a handful of text-editors).
|
|
726
|
+
*/
|
|
727
|
+
function axeDesignerApplyLooksLikeWipe(current, incoming) {
|
|
728
|
+
const currentCount = countWidgetNodes(current);
|
|
729
|
+
const incomingCount = countWidgetNodes(incoming);
|
|
730
|
+
if (currentCount >= 12 && incomingCount < Math.max(8, Math.floor(currentCount * 0.5))) {
|
|
731
|
+
return `apply-layout refused: the new tree has ${incomingCount} widgets but the canvas has ${currentCount}. Use add-widget / update-widget / remove-widget for field edits. Use replaceAll=true only for a brand-new form.`;
|
|
732
|
+
}
|
|
733
|
+
if (layoutHasType(current, 'fieldset-layout') && !layoutHasType(incoming, 'fieldset-layout')) {
|
|
734
|
+
return 'apply-layout refused: the canvas uses fieldset-layout sections. Do not flatten the form. Use add-widget / update-widget / remove-widget for field edits.';
|
|
735
|
+
}
|
|
736
|
+
if (layoutHasType(current, 'form-field') && !layoutHasType(incoming, 'form-field')) {
|
|
737
|
+
return 'apply-layout refused: labeled form-field wrappers were dropped. Keep the existing section/field structure and only change what the user asked.';
|
|
738
|
+
}
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
function countWidgetNodes(value) {
|
|
742
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
743
|
+
return 0;
|
|
744
|
+
}
|
|
745
|
+
const row = value;
|
|
746
|
+
let count = typeof row['type'] === 'string' && row['type'].trim() ? 1 : 0;
|
|
747
|
+
const children = row['children'];
|
|
748
|
+
if (Array.isArray(children)) {
|
|
749
|
+
for (const child of children) {
|
|
750
|
+
count += countWidgetNodes(child);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return count;
|
|
754
|
+
}
|
|
755
|
+
function layoutHasType(value, type) {
|
|
756
|
+
if (!value || typeof value !== 'object') {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
if (Array.isArray(value)) {
|
|
760
|
+
return value.some((item) => layoutHasType(item, type));
|
|
761
|
+
}
|
|
762
|
+
const row = value;
|
|
763
|
+
if (typeof row['type'] === 'string' && row['type'].trim() === type) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
return layoutHasType(row['children'], type);
|
|
767
|
+
}
|
|
768
|
+
//#endregion
|
|
769
|
+
|
|
770
|
+
//#region ----- Imports -----
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region ----- Service -----
|
|
773
|
+
/**
|
|
774
|
+
* Form-template designer AI assist: opens the dedicated form-designer-assistant
|
|
775
|
+
* and advertises designer API callbacks on page context (Page:GetContext / Page:RunAction).
|
|
776
|
+
*/
|
|
777
|
+
class AXEAiFormbuilderDesignerAiAssistService {
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region ----- Lifecycle -----
|
|
780
|
+
constructor() {
|
|
781
|
+
//#region ----- Services & Dependencies -----
|
|
782
|
+
this.launcher = inject(AXMContextualAssistLauncherService, { optional: true });
|
|
783
|
+
this.translationService = inject(AXTranslationService);
|
|
784
|
+
this.toastService = inject(AXToastService);
|
|
785
|
+
this.entityService = inject(AXPEntityService, { optional: true });
|
|
786
|
+
this.pageContext = inject(AXPPageContextRegistry);
|
|
787
|
+
this.realtimeBus = inject(AXMConversationRealtimeBus, { optional: true });
|
|
788
|
+
this.destroyRef = inject(DestroyRef);
|
|
789
|
+
//#endregion
|
|
790
|
+
//#region ----- State Signals -----
|
|
791
|
+
this.designer = null;
|
|
792
|
+
this.lastAppliedJson = '';
|
|
793
|
+
this.pageTitle = '';
|
|
794
|
+
const bus = this.realtimeBus;
|
|
795
|
+
if (!bus) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
merge(bus.messageStream$, bus.messageUpdates$)
|
|
799
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
800
|
+
.subscribe((message) => {
|
|
801
|
+
void this.tryApplyFromAssistMessage(message);
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
//#endregion
|
|
805
|
+
//#region ----- Public API -----
|
|
806
|
+
/**
|
|
807
|
+
* Binds the live designer and advertises add/update/remove/move/apply-layout on page context.
|
|
808
|
+
* Pass the header {@link DestroyRef} so the registration clears when the designer unmounts.
|
|
809
|
+
*/
|
|
810
|
+
bindDesigner(designer, owner) {
|
|
811
|
+
this.designer = designer;
|
|
812
|
+
this.designer.ensureWidgetTreeMeta?.();
|
|
813
|
+
void this.translationService
|
|
814
|
+
.translateAsync('@form-template-management:designerAi.dialog-title')
|
|
815
|
+
.then((title) => {
|
|
816
|
+
this.pageTitle = title;
|
|
817
|
+
});
|
|
818
|
+
if (!owner) {
|
|
819
|
+
this.pageContext.activate(AXE_AI_FORMBUILDER_PAGE_CONTEXT_ID);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
this.pageContext.register({
|
|
823
|
+
owner,
|
|
824
|
+
id: AXE_AI_FORMBUILDER_PAGE_CONTEXT_ID,
|
|
825
|
+
priority: AXPPageContextPriority.Page,
|
|
826
|
+
getSnapshot: () => this.buildPageSnapshot(),
|
|
827
|
+
run: (actionId, args) => this.runPageAction(actionId, args),
|
|
828
|
+
});
|
|
829
|
+
this.pageContext.activate(AXE_AI_FORMBUILDER_PAGE_CONTEXT_ID);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Toggles contextual assist for the designer's current template (close if open, otherwise open).
|
|
833
|
+
*/
|
|
834
|
+
async toggleFromDesigner(designer) {
|
|
835
|
+
const launcher = this.launcher;
|
|
836
|
+
if (!launcher) {
|
|
837
|
+
return this.warnConversationUnavailable();
|
|
838
|
+
}
|
|
839
|
+
if (launcher.isOpen()) {
|
|
840
|
+
launcher.close();
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
return this.runFromDesigner(designer);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Opens contextual assist for the designer's current template and binds the canvas for apply.
|
|
847
|
+
*/
|
|
848
|
+
async runFromDesigner(designer) {
|
|
849
|
+
const launcher = this.launcher;
|
|
850
|
+
if (!launcher) {
|
|
851
|
+
return this.warnConversationUnavailable();
|
|
852
|
+
}
|
|
853
|
+
const svc = designer;
|
|
854
|
+
this.bindDesigner(svc);
|
|
855
|
+
const recordId = svc.templateId()?.trim() || AXE_AI_FORMBUILDER_DESIGNER_DRAFT_ENTITY_ID;
|
|
856
|
+
const entityTitle = await this.translationService.translateAsync('@form-template-management:designerAi.dialog-title');
|
|
857
|
+
this.pageTitle = entityTitle;
|
|
858
|
+
const columns = await this.loadTemplateColumns(recordId);
|
|
859
|
+
const payload = axeBuildFormbuilderDesignerAssistPayload(svc, {
|
|
860
|
+
entityTitle,
|
|
861
|
+
recordId,
|
|
862
|
+
columns,
|
|
863
|
+
});
|
|
864
|
+
await launcher.open({
|
|
865
|
+
kind: AXE_AI_FORMBUILDER_ASSIST_KIND,
|
|
866
|
+
actionName: AXE_AI_FORMBUILDER_ASSIST_ACTION,
|
|
867
|
+
title: entityTitle,
|
|
868
|
+
payload: { ...payload },
|
|
869
|
+
}, { assistId: AXE_AI_FORMBUILDER_DESIGNER_ASSIST_ID, forceNewChat: false });
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Validates designer JSON and loads it onto the bound canvas (does not save).
|
|
874
|
+
*/
|
|
875
|
+
async applyLayoutJson(json, options) {
|
|
876
|
+
const designer = this.designer;
|
|
877
|
+
if (!designer) {
|
|
878
|
+
return {
|
|
879
|
+
applied: false,
|
|
880
|
+
errors: ['The layout designer is not bound. Open the form designer page so page context can apply the canvas.'],
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const parsed = axeParseAndValidateLayoutJson(json);
|
|
884
|
+
if (!parsed.valid || !parsed.normalizedJson) {
|
|
885
|
+
return { applied: false, errors: parsed.errors };
|
|
886
|
+
}
|
|
887
|
+
let root;
|
|
888
|
+
try {
|
|
889
|
+
root = JSON.parse(parsed.normalizedJson);
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
return { applied: false, errors: parsed.errors };
|
|
893
|
+
}
|
|
894
|
+
if (!axeIsDesignerCanvasRoot(root)) {
|
|
895
|
+
return {
|
|
896
|
+
applied: false,
|
|
897
|
+
errors: [
|
|
898
|
+
'Designer updates need a document-layout or page-layout root. Chat block-layout trees are not applied to the canvas.',
|
|
899
|
+
],
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
if (!options?.replaceAll) {
|
|
903
|
+
const wipe = this.detectApplyWipe(root);
|
|
904
|
+
if (wipe) {
|
|
905
|
+
return { applied: false, errors: [wipe] };
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (this.lastAppliedJson === parsed.normalizedJson) {
|
|
909
|
+
return { applied: true, errors: [] };
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
await designer.loadByContent(parsed.normalizedJson);
|
|
913
|
+
designer.unsavedChanges.set(true);
|
|
914
|
+
this.lastAppliedJson = parsed.normalizedJson;
|
|
915
|
+
const title = await this.translationService.translateAsync('@form-template-management:designerAi.loading.complete');
|
|
916
|
+
const content = await this.translationService.translateAsync('@form-template-management:designerAi.applied-hint');
|
|
917
|
+
await this.toastService.show({ color: 'success', title, content });
|
|
918
|
+
return { applied: true, errors: [] };
|
|
919
|
+
}
|
|
920
|
+
catch (error) {
|
|
921
|
+
const message = error instanceof Error ? error.message : 'The layout could not be applied to the designer canvas.';
|
|
922
|
+
return { applied: false, errors: [message] };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
outline() {
|
|
926
|
+
const designer = this.designer;
|
|
927
|
+
if (!designer) {
|
|
928
|
+
return { items: [], errors: [unboundDesignerError()] };
|
|
929
|
+
}
|
|
930
|
+
return { items: axeBuildDesignerOutline(designer.document()), errors: [] };
|
|
931
|
+
}
|
|
932
|
+
addWidget(input) {
|
|
933
|
+
const designer = this.designer;
|
|
934
|
+
if (!designer) {
|
|
935
|
+
return { applied: false, errors: [unboundDesignerError()] };
|
|
936
|
+
}
|
|
937
|
+
const widget = axeResolveWidgetNodeInput(input.widget);
|
|
938
|
+
if (!widget) {
|
|
939
|
+
return { applied: false, errors: ['widget must be an AXPWidgetNode object with a string type.'] };
|
|
940
|
+
}
|
|
941
|
+
const parentHit = this.resolveWidgetRef(input.parentName, input.parentPath);
|
|
942
|
+
if (!parentHit) {
|
|
943
|
+
return { applied: false, errors: [missingWidgetRefError$1('parent', input.parentName, input.parentPath)] };
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
const added = designer.addWidget(parentHit.node, widget);
|
|
947
|
+
designer.ensureWidgetTreeMeta?.();
|
|
948
|
+
const index = axeReadOptionalIndex(input.index);
|
|
949
|
+
if (index != null) {
|
|
950
|
+
moveChildToIndex(parentHit.node, added, index);
|
|
951
|
+
designer.refresh({ widget: parentHit.node });
|
|
952
|
+
}
|
|
953
|
+
designer.unsavedChanges.set(true);
|
|
954
|
+
const outline = axeBuildDesignerOutline(designer.document());
|
|
955
|
+
const addedName = typeof added.name === 'string' ? added.name.trim() : '';
|
|
956
|
+
const addedHit = addedName ? outline.find((item) => item.name === addedName) : undefined;
|
|
957
|
+
return {
|
|
958
|
+
applied: true,
|
|
959
|
+
errors: [],
|
|
960
|
+
...(addedName ? { name: addedName } : {}),
|
|
961
|
+
...(addedHit?.path ? { path: addedHit.path } : {}),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
return { applied: false, errors: [error instanceof Error ? error.message : 'The widget could not be added.'] };
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
updateWidget(input) {
|
|
969
|
+
const designer = this.designer;
|
|
970
|
+
if (!designer) {
|
|
971
|
+
return { applied: false, errors: [unboundDesignerError()] };
|
|
972
|
+
}
|
|
973
|
+
const hit = this.resolveWidgetRef(input.name, input.path);
|
|
974
|
+
if (!hit) {
|
|
975
|
+
return { applied: false, errors: [missingWidgetRefError$1('widget', input.name, input.path)] };
|
|
976
|
+
}
|
|
977
|
+
const options = {};
|
|
978
|
+
if (input.options != null && typeof input.options === 'object' && !Array.isArray(input.options)) {
|
|
979
|
+
Object.assign(options, input.options);
|
|
980
|
+
}
|
|
981
|
+
if (input.validations != null) {
|
|
982
|
+
options['validations'] = input.validations;
|
|
983
|
+
}
|
|
984
|
+
const triggers = axeReadOptionalTriggers(input.triggers);
|
|
985
|
+
const visible = axeReadOptionalVisible(input.visible);
|
|
986
|
+
const patch = {};
|
|
987
|
+
if (Object.keys(options).length > 0) {
|
|
988
|
+
patch['options'] = options;
|
|
989
|
+
}
|
|
990
|
+
if (triggers.set) {
|
|
991
|
+
patch['triggers'] = triggers.value ?? [];
|
|
992
|
+
}
|
|
993
|
+
if (visible.set) {
|
|
994
|
+
patch['visible'] = visible.value;
|
|
995
|
+
}
|
|
996
|
+
if (Object.keys(patch).length === 0) {
|
|
997
|
+
return {
|
|
998
|
+
applied: false,
|
|
999
|
+
errors: ['Provide options, validations, triggers, and/or visible to update.'],
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
designer.update({ widget: hit.node, values: patch, mode: 'update' });
|
|
1004
|
+
designer.unsavedChanges.set(true);
|
|
1005
|
+
return { applied: true, errors: [] };
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
return { applied: false, errors: [error instanceof Error ? error.message : 'The widget could not be updated.'] };
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
removeWidget(input) {
|
|
1012
|
+
const designer = this.designer;
|
|
1013
|
+
if (!designer) {
|
|
1014
|
+
return { applied: false, errors: [unboundDesignerError()] };
|
|
1015
|
+
}
|
|
1016
|
+
const hit = this.resolveWidgetRef(input.name, input.path);
|
|
1017
|
+
if (!hit) {
|
|
1018
|
+
return { applied: false, errors: [missingWidgetRefError$1('widget', input.name, input.path)] };
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
designer.removeWidget(hit.node);
|
|
1022
|
+
designer.unsavedChanges.set(true);
|
|
1023
|
+
return { applied: true, errors: [] };
|
|
1024
|
+
}
|
|
1025
|
+
catch (error) {
|
|
1026
|
+
return { applied: false, errors: [error instanceof Error ? error.message : 'The widget could not be removed.'] };
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
moveWidget(input) {
|
|
1030
|
+
const designer = this.designer;
|
|
1031
|
+
if (!designer) {
|
|
1032
|
+
return { applied: false, errors: [unboundDesignerError()] };
|
|
1033
|
+
}
|
|
1034
|
+
const hit = this.resolveWidgetRef(input.name, input.path);
|
|
1035
|
+
if (!hit?.parent) {
|
|
1036
|
+
return { applied: false, errors: [missingWidgetRefError$1('widget', input.name, input.path)] };
|
|
1037
|
+
}
|
|
1038
|
+
const parentName = axeReadStringArg(input.parentName);
|
|
1039
|
+
const parentPath = axeReadStringArg(input.parentPath);
|
|
1040
|
+
const nextParentNode = parentName || parentPath
|
|
1041
|
+
? this.resolveWidgetRef(input.parentName, input.parentPath)?.node
|
|
1042
|
+
: hit.parent;
|
|
1043
|
+
if (!nextParentNode) {
|
|
1044
|
+
return { applied: false, errors: [missingWidgetRefError$1('parent', input.parentName, input.parentPath)] };
|
|
1045
|
+
}
|
|
1046
|
+
const index = axeReadOptionalIndex(input.index);
|
|
1047
|
+
try {
|
|
1048
|
+
if (nextParentNode !== hit.parent) {
|
|
1049
|
+
const from = hit.parent.children;
|
|
1050
|
+
if (!Array.isArray(from)) {
|
|
1051
|
+
return { applied: false, errors: ['The current parent has no children.'] };
|
|
1052
|
+
}
|
|
1053
|
+
const at = from.indexOf(hit.node);
|
|
1054
|
+
if (at < 0) {
|
|
1055
|
+
return { applied: false, errors: ['The widget is no longer under its parent.'] };
|
|
1056
|
+
}
|
|
1057
|
+
from.splice(at, 1);
|
|
1058
|
+
nextParentNode.children = nextParentNode.children ?? [];
|
|
1059
|
+
const insertAt = index == null
|
|
1060
|
+
? nextParentNode.children.length
|
|
1061
|
+
: Math.max(0, Math.min(index, nextParentNode.children.length));
|
|
1062
|
+
nextParentNode.children.splice(insertAt, 0, hit.node);
|
|
1063
|
+
designer.refresh({ widget: hit.parent });
|
|
1064
|
+
designer.refresh({ widget: nextParentNode });
|
|
1065
|
+
}
|
|
1066
|
+
else if (index != null) {
|
|
1067
|
+
moveChildToIndex(hit.parent, hit.node, index);
|
|
1068
|
+
designer.refresh({ widget: hit.parent });
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
return { applied: false, errors: ['Provide parentName/parentPath or index to move the widget.'] };
|
|
1072
|
+
}
|
|
1073
|
+
designer.unsavedChanges.set(true);
|
|
1074
|
+
return { applied: true, errors: [] };
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
return { applied: false, errors: [error instanceof Error ? error.message : 'The widget could not be moved.'] };
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
//#endregion
|
|
1081
|
+
//#region ----- Page context -----
|
|
1082
|
+
buildPageSnapshot() {
|
|
1083
|
+
return axeBuildFormbuilderDesignerPageContextSnapshot({
|
|
1084
|
+
title: this.pageTitle,
|
|
1085
|
+
outline: this.designer ? this.outline().items : undefined,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
detectApplyWipe(incoming) {
|
|
1089
|
+
const designer = this.designer;
|
|
1090
|
+
if (!designer) {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
const currentRaw = designer.export();
|
|
1095
|
+
const current = JSON.parse(typeof currentRaw === 'string' ? currentRaw : '');
|
|
1096
|
+
return axeDesignerApplyLooksLikeWipe(current, incoming);
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async runPageAction(actionId, args) {
|
|
1103
|
+
if (actionId === AXE_AI_FORMBUILDER_PAGE_ACTION_ADD_WIDGET) {
|
|
1104
|
+
const result = this.addWidget(args);
|
|
1105
|
+
return this.pageActionResult(result.applied, result.errors[0]);
|
|
1106
|
+
}
|
|
1107
|
+
if (actionId === AXE_AI_FORMBUILDER_PAGE_ACTION_UPDATE_WIDGET) {
|
|
1108
|
+
const result = this.updateWidget(args);
|
|
1109
|
+
return this.pageActionResult(result.applied, result.errors[0]);
|
|
1110
|
+
}
|
|
1111
|
+
if (actionId === AXE_AI_FORMBUILDER_PAGE_ACTION_REMOVE_WIDGET) {
|
|
1112
|
+
const result = this.removeWidget(args);
|
|
1113
|
+
return this.pageActionResult(result.applied, result.errors[0]);
|
|
1114
|
+
}
|
|
1115
|
+
if (actionId === AXE_AI_FORMBUILDER_PAGE_ACTION_MOVE_WIDGET) {
|
|
1116
|
+
const result = this.moveWidget(args);
|
|
1117
|
+
return this.pageActionResult(result.applied, result.errors[0]);
|
|
1118
|
+
}
|
|
1119
|
+
if (actionId === AXE_AI_FORMBUILDER_PAGE_ACTION_APPLY_LAYOUT) {
|
|
1120
|
+
const json = axeResolvePageContextLayoutJson(args);
|
|
1121
|
+
const result = await this.applyLayoutJson(json, { replaceAll: axeReadReplaceAllFlag(args) });
|
|
1122
|
+
return this.pageActionResult(result.applied, result.errors[0] ?? 'Layout was not applied.');
|
|
1123
|
+
}
|
|
1124
|
+
return {
|
|
1125
|
+
success: false,
|
|
1126
|
+
message: `Unknown page action "${actionId}".`,
|
|
1127
|
+
snapshot: this.buildPageSnapshot(),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
pageActionResult(success, error) {
|
|
1131
|
+
return {
|
|
1132
|
+
success,
|
|
1133
|
+
message: success ? undefined : (error ?? 'The designer action failed.'),
|
|
1134
|
+
snapshot: this.buildPageSnapshot(),
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
//#endregion
|
|
1138
|
+
//#region ----- Auto apply -----
|
|
1139
|
+
async tryApplyFromAssistMessage(message) {
|
|
1140
|
+
if (!this.designer || !axeAssistMessageReadyToApply(message)) {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const json = axeDesignerLayoutJsonFromAssistMessage(message);
|
|
1144
|
+
if (!json) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
await this.applyLayoutJson(json);
|
|
1148
|
+
}
|
|
1149
|
+
//#endregion
|
|
1150
|
+
//#region ----- Warnings -----
|
|
1151
|
+
async warnConversationUnavailable() {
|
|
1152
|
+
const title = await this.translationService.translateAsync('@general:messages.generic.warning.title');
|
|
1153
|
+
const content = await this.translationService.translateAsync('@form-template-management:designerAi.no-conversation-module');
|
|
1154
|
+
await this.toastService.show({ color: 'warning', title, content });
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
//#endregion
|
|
1158
|
+
//#region ----- Template record -----
|
|
1159
|
+
async loadTemplateColumns(recordId) {
|
|
1160
|
+
if (!this.entityService || recordId === AXE_AI_FORMBUILDER_DESIGNER_DRAFT_ENTITY_ID) {
|
|
1161
|
+
return [];
|
|
1162
|
+
}
|
|
1163
|
+
try {
|
|
1164
|
+
const row = (await this.entityService.withEntity(AXE_AI_FORMBUILDER_DESIGNER_ENTITY_TYPE).data().byKey(recordId));
|
|
1165
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
1166
|
+
return [];
|
|
1167
|
+
}
|
|
1168
|
+
return axeBuildFormTemplateRecordAssistColumns(row, (value) => this.asAssistDisplayText(value));
|
|
1169
|
+
}
|
|
1170
|
+
catch {
|
|
1171
|
+
return [];
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
resolveWidgetRef(name, path) {
|
|
1175
|
+
const designer = this.designer;
|
|
1176
|
+
if (!designer) {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
return axeFindDesignerWidget(designer.document(), {
|
|
1180
|
+
name: axeReadStringArg(name),
|
|
1181
|
+
path: axeReadStringArg(path),
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
asAssistDisplayText(value) {
|
|
1185
|
+
if (typeof value === 'string') {
|
|
1186
|
+
return value.trim();
|
|
1187
|
+
}
|
|
1188
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
1189
|
+
return String(value);
|
|
1190
|
+
}
|
|
1191
|
+
const resolved = this.translationService.resolve(coerceUnknownToMultiLanguageString(value))?.trim();
|
|
1192
|
+
return resolved ?? '';
|
|
1193
|
+
}
|
|
1194
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderDesignerAiAssistService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1195
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderDesignerAiAssistService, providedIn: 'root' }); }
|
|
1196
|
+
}
|
|
1197
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderDesignerAiAssistService, decorators: [{
|
|
1198
|
+
type: Injectable,
|
|
1199
|
+
args: [{ providedIn: 'root' }]
|
|
1200
|
+
}], ctorParameters: () => [] });
|
|
1201
|
+
function unboundDesignerError() {
|
|
1202
|
+
return 'The layout designer is not bound. Open the form designer page so the canvas can be edited.';
|
|
1203
|
+
}
|
|
1204
|
+
function missingWidgetRefError$1(kind, name, path) {
|
|
1205
|
+
const nameText = axeReadStringArg(name);
|
|
1206
|
+
const pathText = axeReadStringArg(path);
|
|
1207
|
+
if (nameText || pathText) {
|
|
1208
|
+
return `No ${kind} found for ${nameText ? `name "${nameText}"` : `path "${pathText}"`}. Call Page:GetContext and use a name from snapshot.outline.`;
|
|
1209
|
+
}
|
|
1210
|
+
return `Provide ${kind}Name or ${kind}Path from snapshot.outline (Page:GetContext).`;
|
|
1211
|
+
}
|
|
1212
|
+
function moveChildToIndex(parent, child, index) {
|
|
1213
|
+
const children = parent.children;
|
|
1214
|
+
if (!Array.isArray(children)) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const at = children.indexOf(child);
|
|
1218
|
+
if (at < 0) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
children.splice(at, 1);
|
|
1222
|
+
const insertAt = Math.max(0, Math.min(index, children.length));
|
|
1223
|
+
children.splice(insertAt, 0, child);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
//#region ----- Formbuilder designer footer -----
|
|
1227
|
+
/** Registers the layout designer footer-end magic-icon button for AI assist. */
|
|
1228
|
+
const AXEAiFormbuilderDesignerFooterHookProvider = {
|
|
1229
|
+
key: AXPLayoutDesignerFooterHookKeys.end,
|
|
1230
|
+
priority: 40,
|
|
1231
|
+
execute: (payload) => {
|
|
1232
|
+
const assist = inject(AXEAiFormbuilderDesignerAiAssistService);
|
|
1233
|
+
assist.bindDesigner(payload.designer, payload.owner);
|
|
1234
|
+
payload.items.push({
|
|
1235
|
+
id: 'extensions-ai-formbuilder-layout-assist-footer',
|
|
1236
|
+
priority: 20,
|
|
1237
|
+
labelKey: '@form-template-management:designerAi.menu',
|
|
1238
|
+
iconClass: 'fa-solid fa-wand-magic-sparkles',
|
|
1239
|
+
run: () => {
|
|
1240
|
+
void assist.toggleFromDesigner(payload.designer);
|
|
1241
|
+
},
|
|
1242
|
+
});
|
|
1243
|
+
return payload;
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1246
|
+
//#endregion
|
|
1247
|
+
|
|
1248
|
+
//#region ----- Formbuilder designer Help menu -----
|
|
1249
|
+
/** Registers the layout designer Help-menu entry for AI assist (wand icon). */
|
|
1250
|
+
const AXEAiFormbuilderDesignerHelpMenuHookProvider = {
|
|
1251
|
+
key: AXPLayoutDesignerHeaderMenuHookKeys.submenu,
|
|
1252
|
+
priority: 40,
|
|
1253
|
+
execute: (payload) => {
|
|
1254
|
+
if (payload.parentMenuId !== 'help') {
|
|
1255
|
+
return payload;
|
|
1256
|
+
}
|
|
1257
|
+
const assist = inject(AXEAiFormbuilderDesignerAiAssistService);
|
|
1258
|
+
assist.bindDesigner(payload.designer, payload.owner);
|
|
1259
|
+
payload.items.push({
|
|
1260
|
+
id: 'extensions-ai-formbuilder-layout-assist-help',
|
|
1261
|
+
priority: 20,
|
|
1262
|
+
labelKey: '@form-template-management:designerAi.menu',
|
|
1263
|
+
iconClass: 'fa-solid fa-wand-magic-sparkles',
|
|
1264
|
+
run: () => {
|
|
1265
|
+
void assist.toggleFromDesigner(payload.designer);
|
|
1266
|
+
},
|
|
1267
|
+
});
|
|
1268
|
+
return payload;
|
|
1269
|
+
},
|
|
1270
|
+
};
|
|
1271
|
+
//#endregion
|
|
1272
|
+
|
|
1273
|
+
//#region ---- Imports ----
|
|
1274
|
+
//#endregion
|
|
1275
|
+
//#region ---- Formbuilder node segment preset ----
|
|
1276
|
+
const FORM_BUILDER_NODE_SEGMENT = {
|
|
1277
|
+
name: 'node',
|
|
1278
|
+
title: '@ai-management:agents.entities.agent.fields.output-contract.transcript-segment.options.node',
|
|
1279
|
+
applicableKinds: ['object'],
|
|
1280
|
+
contract: {
|
|
1281
|
+
kind: 'object',
|
|
1282
|
+
transcriptSegment: 'node',
|
|
1283
|
+
validationCommandName: 'AiFormbuilder:ValidateLayoutJsonForAi',
|
|
1284
|
+
structuredRetries: 3,
|
|
1285
|
+
schemaSummary: 'Chat UI: block-layout or table-layout root with mode edit/view. Form Template designer: document-layout → page-layout → fieldset/flex/grid. No raw CSS keys.',
|
|
1286
|
+
},
|
|
1287
|
+
};
|
|
1288
|
+
/**
|
|
1289
|
+
* Enriches the built-in `node` transcript segment with formbuilder validation and schema hints.
|
|
1290
|
+
*/
|
|
1291
|
+
class AXPAiFormbuilderOutputContractTranscriptSegmentProvider {
|
|
1292
|
+
provide() {
|
|
1293
|
+
return [FORM_BUILDER_NODE_SEGMENT];
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
//#endregion
|
|
1297
|
+
|
|
1298
|
+
//#region ----- Imports -----
|
|
1299
|
+
//#endregion
|
|
1300
|
+
//#region ----- Module -----
|
|
1301
|
+
/**
|
|
1302
|
+
* Optional AI assist on the form-template designer (Help menu + footer magic icon).
|
|
1303
|
+
* Register next to {@link AXEAIConversationExtensionModule} in the application.
|
|
1304
|
+
*/
|
|
1305
|
+
class AXEAIFormBuilderExtensionModule {
|
|
1306
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIFormBuilderExtensionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
1307
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXEAIFormBuilderExtensionModule }); }
|
|
1308
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIFormBuilderExtensionModule, providers: [
|
|
1309
|
+
provideCommandSetups([
|
|
1310
|
+
{
|
|
1311
|
+
key: AXE_AI_FORMBUILDER_VALIDATE_LAYOUT_COMMAND,
|
|
1312
|
+
command: () => Promise.resolve().then(function () { return validateLayoutJsonForAi_command; }).then((c) => c.AXEAiFormbuilderValidateLayoutJsonForAiCommand),
|
|
1313
|
+
},
|
|
1314
|
+
{
|
|
1315
|
+
key: AXE_AI_FORMBUILDER_APPLY_LAYOUT_COMMAND,
|
|
1316
|
+
command: () => Promise.resolve().then(function () { return applyLayoutJsonForAi_command; }).then((c) => c.AXEAiFormbuilderApplyLayoutJsonForAiCommand),
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
key: AXE_AI_FORMBUILDER_OUTLINE_LAYOUT_COMMAND,
|
|
1320
|
+
command: () => Promise.resolve().then(function () { return outlineLayoutForAi_command; }).then((c) => c.AXEAiFormbuilderOutlineLayoutForAiCommand),
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
key: AXE_AI_FORMBUILDER_ADD_WIDGET_COMMAND,
|
|
1324
|
+
command: () => Promise.resolve().then(function () { return addWidgetForAi_command; }).then((c) => c.AXEAiFormbuilderAddWidgetForAiCommand),
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
key: AXE_AI_FORMBUILDER_UPDATE_WIDGET_COMMAND,
|
|
1328
|
+
command: () => Promise.resolve().then(function () { return updateWidgetForAi_command; }).then((c) => c.AXEAiFormbuilderUpdateWidgetForAiCommand),
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
key: AXE_AI_FORMBUILDER_REMOVE_WIDGET_COMMAND,
|
|
1332
|
+
command: () => Promise.resolve().then(function () { return removeWidgetForAi_command; }).then((c) => c.AXEAiFormbuilderRemoveWidgetForAiCommand),
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
key: AXE_AI_FORMBUILDER_MOVE_WIDGET_COMMAND,
|
|
1336
|
+
command: () => Promise.resolve().then(function () { return moveWidgetForAi_command; }).then((c) => c.AXEAiFormbuilderMoveWidgetForAiCommand),
|
|
1337
|
+
},
|
|
1338
|
+
]),
|
|
1339
|
+
{
|
|
1340
|
+
provide: AXP_HOOK_PROVIDER,
|
|
1341
|
+
multi: true,
|
|
1342
|
+
useValue: AXEAiFormbuilderDesignerHelpMenuHookProvider,
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
provide: AXP_HOOK_PROVIDER,
|
|
1346
|
+
multi: true,
|
|
1347
|
+
useValue: AXEAiFormbuilderDesignerFooterHookProvider,
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
provide: AXP_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_PROVIDER,
|
|
1351
|
+
useClass: AXPAiFormbuilderOutputContractTranscriptSegmentProvider,
|
|
1352
|
+
multi: true,
|
|
1353
|
+
},
|
|
1354
|
+
] }); }
|
|
1355
|
+
}
|
|
1356
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIFormBuilderExtensionModule, decorators: [{
|
|
1357
|
+
type: NgModule,
|
|
1358
|
+
args: [{
|
|
1359
|
+
providers: [
|
|
1360
|
+
provideCommandSetups([
|
|
1361
|
+
{
|
|
1362
|
+
key: AXE_AI_FORMBUILDER_VALIDATE_LAYOUT_COMMAND,
|
|
1363
|
+
command: () => Promise.resolve().then(function () { return validateLayoutJsonForAi_command; }).then((c) => c.AXEAiFormbuilderValidateLayoutJsonForAiCommand),
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
key: AXE_AI_FORMBUILDER_APPLY_LAYOUT_COMMAND,
|
|
1367
|
+
command: () => Promise.resolve().then(function () { return applyLayoutJsonForAi_command; }).then((c) => c.AXEAiFormbuilderApplyLayoutJsonForAiCommand),
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
key: AXE_AI_FORMBUILDER_OUTLINE_LAYOUT_COMMAND,
|
|
1371
|
+
command: () => Promise.resolve().then(function () { return outlineLayoutForAi_command; }).then((c) => c.AXEAiFormbuilderOutlineLayoutForAiCommand),
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
key: AXE_AI_FORMBUILDER_ADD_WIDGET_COMMAND,
|
|
1375
|
+
command: () => Promise.resolve().then(function () { return addWidgetForAi_command; }).then((c) => c.AXEAiFormbuilderAddWidgetForAiCommand),
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
key: AXE_AI_FORMBUILDER_UPDATE_WIDGET_COMMAND,
|
|
1379
|
+
command: () => Promise.resolve().then(function () { return updateWidgetForAi_command; }).then((c) => c.AXEAiFormbuilderUpdateWidgetForAiCommand),
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
key: AXE_AI_FORMBUILDER_REMOVE_WIDGET_COMMAND,
|
|
1383
|
+
command: () => Promise.resolve().then(function () { return removeWidgetForAi_command; }).then((c) => c.AXEAiFormbuilderRemoveWidgetForAiCommand),
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
key: AXE_AI_FORMBUILDER_MOVE_WIDGET_COMMAND,
|
|
1387
|
+
command: () => Promise.resolve().then(function () { return moveWidgetForAi_command; }).then((c) => c.AXEAiFormbuilderMoveWidgetForAiCommand),
|
|
1388
|
+
},
|
|
1389
|
+
]),
|
|
1390
|
+
{
|
|
1391
|
+
provide: AXP_HOOK_PROVIDER,
|
|
1392
|
+
multi: true,
|
|
1393
|
+
useValue: AXEAiFormbuilderDesignerHelpMenuHookProvider,
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
provide: AXP_HOOK_PROVIDER,
|
|
1397
|
+
multi: true,
|
|
1398
|
+
useValue: AXEAiFormbuilderDesignerFooterHookProvider,
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
provide: AXP_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_PROVIDER,
|
|
1402
|
+
useClass: AXPAiFormbuilderOutputContractTranscriptSegmentProvider,
|
|
1403
|
+
multi: true,
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
}]
|
|
1407
|
+
}] });
|
|
1408
|
+
|
|
1409
|
+
//#region ----- Imports -----
|
|
1410
|
+
//#endregion
|
|
1411
|
+
//#region ----- Outline -----
|
|
1412
|
+
function axeOutlineLayoutJson(input) {
|
|
1413
|
+
const parsed = parseLayoutTree(input);
|
|
1414
|
+
if (!parsed.root) {
|
|
1415
|
+
return { items: [], errors: parsed.errors };
|
|
1416
|
+
}
|
|
1417
|
+
return { items: axeBuildDesignerOutline(parsed.root), errors: [] };
|
|
1418
|
+
}
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region ----- Mutations -----
|
|
1421
|
+
function axeAddWidgetToLayoutJson(input) {
|
|
1422
|
+
const parsed = parseLayoutTree(input);
|
|
1423
|
+
if (!parsed.root) {
|
|
1424
|
+
return fail(parsed.errors);
|
|
1425
|
+
}
|
|
1426
|
+
const widget = axeResolveWidgetNodeInput(input.widget);
|
|
1427
|
+
if (!widget) {
|
|
1428
|
+
return fail(['widget must be an AXPWidgetNode object with a string type.']);
|
|
1429
|
+
}
|
|
1430
|
+
const parent = axeFindDesignerWidget(parsed.root, {
|
|
1431
|
+
name: axeReadStringArg(input.parentName),
|
|
1432
|
+
path: axeReadStringArg(input.parentPath),
|
|
1433
|
+
});
|
|
1434
|
+
if (!parent) {
|
|
1435
|
+
return fail([missingWidgetRefError('parent', input.parentName, input.parentPath)]);
|
|
1436
|
+
}
|
|
1437
|
+
parent.node.children = parent.node.children ?? [];
|
|
1438
|
+
const index = axeReadOptionalIndex(input.index);
|
|
1439
|
+
const insertAt = index == null ? parent.node.children.length : Math.max(0, Math.min(index, parent.node.children.length));
|
|
1440
|
+
parent.node.children.splice(insertAt, 0, widget);
|
|
1441
|
+
return success(parsed.root, widget);
|
|
1442
|
+
}
|
|
1443
|
+
function axeUpdateWidgetInLayoutJson(input) {
|
|
1444
|
+
const parsed = parseLayoutTree(input);
|
|
1445
|
+
if (!parsed.root) {
|
|
1446
|
+
return fail(parsed.errors);
|
|
1447
|
+
}
|
|
1448
|
+
const hit = axeFindDesignerWidget(parsed.root, {
|
|
1449
|
+
name: axeReadStringArg(input.name),
|
|
1450
|
+
path: axeReadStringArg(input.path),
|
|
1451
|
+
});
|
|
1452
|
+
if (!hit) {
|
|
1453
|
+
return fail([missingWidgetRefError('widget', input.name, input.path)]);
|
|
1454
|
+
}
|
|
1455
|
+
const options = {
|
|
1456
|
+
...(hit.node.options && typeof hit.node.options === 'object' && !Array.isArray(hit.node.options)
|
|
1457
|
+
? hit.node.options
|
|
1458
|
+
: {}),
|
|
1459
|
+
};
|
|
1460
|
+
if (input.options != null && typeof input.options === 'object' && !Array.isArray(input.options)) {
|
|
1461
|
+
Object.assign(options, input.options);
|
|
1462
|
+
}
|
|
1463
|
+
if (input.validations != null) {
|
|
1464
|
+
options['validations'] = input.validations;
|
|
1465
|
+
}
|
|
1466
|
+
const triggers = axeReadOptionalTriggers(input.triggers);
|
|
1467
|
+
const visible = axeReadOptionalVisible(input.visible);
|
|
1468
|
+
const touchedOptions = input.options != null || input.validations != null;
|
|
1469
|
+
if (!touchedOptions && !triggers.set && !visible.set) {
|
|
1470
|
+
return fail(['Provide options, validations, triggers, and/or visible to update.']);
|
|
1471
|
+
}
|
|
1472
|
+
if (touchedOptions) {
|
|
1473
|
+
hit.node.options = options;
|
|
1474
|
+
}
|
|
1475
|
+
if (triggers.set) {
|
|
1476
|
+
hit.node.triggers = triggers.value ?? [];
|
|
1477
|
+
}
|
|
1478
|
+
if (visible.set) {
|
|
1479
|
+
hit.node.visible = visible.value;
|
|
1480
|
+
}
|
|
1481
|
+
return success(parsed.root, hit.node, hit.path);
|
|
1482
|
+
}
|
|
1483
|
+
function axeRemoveWidgetFromLayoutJson(input) {
|
|
1484
|
+
const parsed = parseLayoutTree(input);
|
|
1485
|
+
if (!parsed.root) {
|
|
1486
|
+
return fail(parsed.errors);
|
|
1487
|
+
}
|
|
1488
|
+
const hit = axeFindDesignerWidget(parsed.root, {
|
|
1489
|
+
name: axeReadStringArg(input.name),
|
|
1490
|
+
path: axeReadStringArg(input.path),
|
|
1491
|
+
});
|
|
1492
|
+
if (!hit) {
|
|
1493
|
+
return fail([missingWidgetRefError('widget', input.name, input.path)]);
|
|
1494
|
+
}
|
|
1495
|
+
if (!hit.parent) {
|
|
1496
|
+
return fail(['Cannot remove the root widget.']);
|
|
1497
|
+
}
|
|
1498
|
+
const children = hit.parent.children;
|
|
1499
|
+
if (!Array.isArray(children)) {
|
|
1500
|
+
return fail(['The parent has no children.']);
|
|
1501
|
+
}
|
|
1502
|
+
const at = children.indexOf(hit.node);
|
|
1503
|
+
if (at < 0) {
|
|
1504
|
+
return fail(['The widget is no longer under its parent.']);
|
|
1505
|
+
}
|
|
1506
|
+
children.splice(at, 1);
|
|
1507
|
+
return { applied: true, errors: [], json: JSON.stringify(parsed.root) };
|
|
1508
|
+
}
|
|
1509
|
+
function axeMoveWidgetInLayoutJson(input) {
|
|
1510
|
+
const parsed = parseLayoutTree(input);
|
|
1511
|
+
if (!parsed.root) {
|
|
1512
|
+
return fail(parsed.errors);
|
|
1513
|
+
}
|
|
1514
|
+
const hit = axeFindDesignerWidget(parsed.root, {
|
|
1515
|
+
name: axeReadStringArg(input.name),
|
|
1516
|
+
path: axeReadStringArg(input.path),
|
|
1517
|
+
});
|
|
1518
|
+
if (!hit?.parent) {
|
|
1519
|
+
return fail([missingWidgetRefError('widget', input.name, input.path)]);
|
|
1520
|
+
}
|
|
1521
|
+
const parentName = axeReadStringArg(input.parentName);
|
|
1522
|
+
const parentPath = axeReadStringArg(input.parentPath);
|
|
1523
|
+
const nextParent = parentName || parentPath
|
|
1524
|
+
? axeFindDesignerWidget(parsed.root, { name: parentName, path: parentPath })?.node
|
|
1525
|
+
: hit.parent;
|
|
1526
|
+
if (!nextParent) {
|
|
1527
|
+
return fail([missingWidgetRefError('parent', input.parentName, input.parentPath)]);
|
|
1528
|
+
}
|
|
1529
|
+
const index = axeReadOptionalIndex(input.index);
|
|
1530
|
+
const from = hit.parent.children;
|
|
1531
|
+
if (!Array.isArray(from)) {
|
|
1532
|
+
return fail(['The current parent has no children.']);
|
|
1533
|
+
}
|
|
1534
|
+
const at = from.indexOf(hit.node);
|
|
1535
|
+
if (at < 0) {
|
|
1536
|
+
return fail(['The widget is no longer under its parent.']);
|
|
1537
|
+
}
|
|
1538
|
+
if (nextParent === hit.parent && index == null) {
|
|
1539
|
+
return fail(['Provide parentName/parentPath or index to move the widget.']);
|
|
1540
|
+
}
|
|
1541
|
+
from.splice(at, 1);
|
|
1542
|
+
nextParent.children = nextParent.children ?? [];
|
|
1543
|
+
const insertAt = index == null
|
|
1544
|
+
? nextParent.children.length
|
|
1545
|
+
: Math.max(0, Math.min(index, nextParent.children.length));
|
|
1546
|
+
nextParent.children.splice(insertAt, 0, hit.node);
|
|
1547
|
+
return success(parsed.root, hit.node);
|
|
1548
|
+
}
|
|
1549
|
+
//#endregion
|
|
1550
|
+
//#region ----- Parse -----
|
|
1551
|
+
function parseLayoutTree(input) {
|
|
1552
|
+
const raw = axeResolveLayoutJsonInput(input);
|
|
1553
|
+
if (!raw) {
|
|
1554
|
+
return { root: null, errors: ['Missing json. Pass the AXPWidgetNode tree as json.'] };
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
const parsed = JSON.parse(raw);
|
|
1558
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1559
|
+
return { root: null, errors: ['json must be an AXPWidgetNode object.'] };
|
|
1560
|
+
}
|
|
1561
|
+
const type = parsed['type'];
|
|
1562
|
+
if (typeof type !== 'string' || !type.trim()) {
|
|
1563
|
+
return { root: null, errors: ['json type must be a widget registry name.'] };
|
|
1564
|
+
}
|
|
1565
|
+
return { root: parsed, errors: [] };
|
|
1566
|
+
}
|
|
1567
|
+
catch (error) {
|
|
1568
|
+
return {
|
|
1569
|
+
root: null,
|
|
1570
|
+
errors: [error instanceof Error ? `JSON parse error: ${error.message}` : 'Invalid JSON.'],
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function success(root, node, knownPath) {
|
|
1575
|
+
const json = JSON.stringify(root);
|
|
1576
|
+
const name = typeof node.name === 'string' ? node.name.trim() : '';
|
|
1577
|
+
const outline = axeBuildDesignerOutline(root);
|
|
1578
|
+
const hit = name
|
|
1579
|
+
? outline.find((item) => item.name === name)
|
|
1580
|
+
: knownPath
|
|
1581
|
+
? outline.find((item) => item.path === knownPath)
|
|
1582
|
+
: undefined;
|
|
1583
|
+
return {
|
|
1584
|
+
applied: true,
|
|
1585
|
+
errors: [],
|
|
1586
|
+
json,
|
|
1587
|
+
...(name ? { name } : {}),
|
|
1588
|
+
...(hit?.path || knownPath ? { path: hit?.path ?? knownPath } : {}),
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
function fail(errors) {
|
|
1592
|
+
return { applied: false, errors };
|
|
1593
|
+
}
|
|
1594
|
+
function missingWidgetRefError(kind, name, path) {
|
|
1595
|
+
const nameText = axeReadStringArg(name);
|
|
1596
|
+
const pathText = axeReadStringArg(path);
|
|
1597
|
+
if (nameText || pathText) {
|
|
1598
|
+
return `No ${kind} found for ${nameText ? `name "${nameText}"` : `path "${pathText}"`}. Call OutlineLayoutForAi with the same json.`;
|
|
1599
|
+
}
|
|
1600
|
+
return `Provide ${kind}Name or ${kind}Path from OutlineLayoutForAi.`;
|
|
1601
|
+
}
|
|
1602
|
+
//#endregion
|
|
1603
|
+
|
|
1604
|
+
//#endregion
|
|
1605
|
+
//#region ----- Command -----
|
|
1606
|
+
class AXEAiFormbuilderValidateLayoutJsonForAiCommand {
|
|
1607
|
+
async execute(input) {
|
|
1608
|
+
const raw = axeResolveLayoutJsonInput(input);
|
|
1609
|
+
const data = axeParseAndValidateLayoutJson(raw);
|
|
1610
|
+
return {
|
|
1611
|
+
success: true,
|
|
1612
|
+
kind: 'object',
|
|
1613
|
+
data,
|
|
1614
|
+
...(data.valid ? {} : { message: { text: data.errors[0] ?? 'Invalid layout JSON.' } }),
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderValidateLayoutJsonForAiCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1618
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderValidateLayoutJsonForAiCommand, providedIn: 'root' }); }
|
|
1619
|
+
}
|
|
1620
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderValidateLayoutJsonForAiCommand, decorators: [{
|
|
1621
|
+
type: Injectable,
|
|
1622
|
+
args: [{ providedIn: 'root' }]
|
|
1623
|
+
}] });
|
|
1624
|
+
|
|
1625
|
+
var validateLayoutJsonForAi_command = /*#__PURE__*/Object.freeze({
|
|
1626
|
+
__proto__: null,
|
|
1627
|
+
AXEAiFormbuilderValidateLayoutJsonForAiCommand: AXEAiFormbuilderValidateLayoutJsonForAiCommand
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* Build-time only. Do not import from Angular modules.
|
|
1632
|
+
* Source: validate-layout-json-for-ai.command.ts — AXEAiFormbuilderValidateLayoutJsonForAiCommand.execute
|
|
1633
|
+
*/
|
|
1634
|
+
const axeAiFormbuilderValidateLayoutJsonForAiCommandDefinition = defineCommand({
|
|
1635
|
+
key: 'AiFormbuilder:ValidateLayoutJsonForAi',
|
|
1636
|
+
title: 'Validate widget layout JSON for AI (form template designer)',
|
|
1637
|
+
description: 'Parses a JSON string and checks it is a single root AXPWidgetNode (object with string type, optional children/options). Rejects raw CSS/DOM-style keys.',
|
|
1638
|
+
executionMode: 'frontend',
|
|
1639
|
+
inputs: [
|
|
1640
|
+
{
|
|
1641
|
+
name: 'json',
|
|
1642
|
+
title: 'Layout JSON string',
|
|
1643
|
+
description: 'Stringified widget tree: one root object with "type" (registry name). Optional markdown/json fences are stripped.',
|
|
1644
|
+
interface: { name: 'large-text-editor' },
|
|
1645
|
+
validations: [{ rule: 'required' }],
|
|
1646
|
+
},
|
|
1647
|
+
],
|
|
1648
|
+
outcomes: {
|
|
1649
|
+
mode: 'static',
|
|
1650
|
+
values: ['success', 'failure'],
|
|
1651
|
+
},
|
|
1652
|
+
ai: {
|
|
1653
|
+
shortDescription: 'Validate specialist JSON is parseable and shaped like AXPWidgetNode before returning it to chat.',
|
|
1654
|
+
tags: ['ai-formbuilder', 'layout', 'ai', 'validation'],
|
|
1655
|
+
},
|
|
1656
|
+
capabilities: ['ai'],
|
|
1657
|
+
categories: [AXP_COMMAND_DEFINITION_CATEGORY_AI_TOOLS],
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
//#endregion
|
|
1661
|
+
//#region ----- Command -----
|
|
1662
|
+
/** Applies validated designer JSON to the open form-template canvas. */
|
|
1663
|
+
class AXEAiFormbuilderApplyLayoutJsonForAiCommand {
|
|
1664
|
+
constructor() {
|
|
1665
|
+
//#region ----- Services & Dependencies -----
|
|
1666
|
+
this.assist = inject(AXEAiFormbuilderDesignerAiAssistService);
|
|
1667
|
+
}
|
|
1668
|
+
//#endregion
|
|
1669
|
+
async execute(input) {
|
|
1670
|
+
const raw = axeResolveLayoutJsonInput(input);
|
|
1671
|
+
const data = await this.assist.applyLayoutJson(raw);
|
|
1672
|
+
return {
|
|
1673
|
+
success: true,
|
|
1674
|
+
kind: 'object',
|
|
1675
|
+
data,
|
|
1676
|
+
...(data.applied ? {} : { message: { text: data.errors[0] ?? 'Layout was not applied.' } }),
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderApplyLayoutJsonForAiCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1680
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderApplyLayoutJsonForAiCommand, providedIn: 'root' }); }
|
|
1681
|
+
}
|
|
1682
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderApplyLayoutJsonForAiCommand, decorators: [{
|
|
1683
|
+
type: Injectable,
|
|
1684
|
+
args: [{ providedIn: 'root' }]
|
|
1685
|
+
}] });
|
|
1686
|
+
|
|
1687
|
+
var applyLayoutJsonForAi_command = /*#__PURE__*/Object.freeze({
|
|
1688
|
+
__proto__: null,
|
|
1689
|
+
AXEAiFormbuilderApplyLayoutJsonForAiCommand: AXEAiFormbuilderApplyLayoutJsonForAiCommand
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Build-time only. Do not import from Angular modules.
|
|
1694
|
+
* Source: apply-layout-json-for-ai.command.ts — AXEAiFormbuilderApplyLayoutJsonForAiCommand.execute
|
|
1695
|
+
*/
|
|
1696
|
+
const axeAiFormbuilderApplyLayoutJsonForAiCommandDefinition = defineCommand({
|
|
1697
|
+
key: 'AiFormbuilder:ApplyLayoutJsonForAi',
|
|
1698
|
+
title: 'Apply widget layout JSON to the form designer',
|
|
1699
|
+
description: 'Validates a root AXPWidgetNode and loads it onto the open Form Template designer canvas (document-layout or page-layout). Prefer Page:RunAction apply-layout on form-designer-assistant for live canvas writes. Does not save — the user uses File → Save.',
|
|
1700
|
+
executionMode: 'frontend',
|
|
1701
|
+
inputs: [
|
|
1702
|
+
{
|
|
1703
|
+
name: 'json',
|
|
1704
|
+
title: 'Layout JSON string',
|
|
1705
|
+
description: 'Stringified designer widget tree. Root must be document-layout (form template) or page-layout (widget template).',
|
|
1706
|
+
interface: { name: 'large-text-editor' },
|
|
1707
|
+
validations: [{ rule: 'required' }],
|
|
1708
|
+
},
|
|
1709
|
+
],
|
|
1710
|
+
outcomes: {
|
|
1711
|
+
mode: 'static',
|
|
1712
|
+
values: ['success', 'failure'],
|
|
1713
|
+
},
|
|
1714
|
+
ai: {
|
|
1715
|
+
shortDescription: 'Write validated designer JSON onto the open form-template canvas so the user sees the change immediately.',
|
|
1716
|
+
tags: ['ai-formbuilder', 'layout', 'ai', 'apply'],
|
|
1717
|
+
},
|
|
1718
|
+
capabilities: ['ai'],
|
|
1719
|
+
categories: [AXP_COMMAND_DEFINITION_CATEGORY_AI_TOOLS],
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
//#endregion
|
|
1723
|
+
//#region ----- Command -----
|
|
1724
|
+
class AXEAiFormbuilderOutlineLayoutForAiCommand {
|
|
1725
|
+
async execute(input = {}) {
|
|
1726
|
+
const data = axeOutlineLayoutJson(input);
|
|
1727
|
+
return {
|
|
1728
|
+
success: true,
|
|
1729
|
+
kind: 'object',
|
|
1730
|
+
data,
|
|
1731
|
+
...(data.errors[0] ? { message: { text: data.errors[0] } } : {}),
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderOutlineLayoutForAiCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1735
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderOutlineLayoutForAiCommand, providedIn: 'root' }); }
|
|
1736
|
+
}
|
|
1737
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderOutlineLayoutForAiCommand, decorators: [{
|
|
1738
|
+
type: Injectable,
|
|
1739
|
+
args: [{ providedIn: 'root' }]
|
|
1740
|
+
}] });
|
|
1741
|
+
|
|
1742
|
+
var outlineLayoutForAi_command = /*#__PURE__*/Object.freeze({
|
|
1743
|
+
__proto__: null,
|
|
1744
|
+
AXEAiFormbuilderOutlineLayoutForAiCommand: AXEAiFormbuilderOutlineLayoutForAiCommand
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* Build-time only. Do not import from Angular modules.
|
|
1749
|
+
* Source: outline-layout-for-ai.command.ts — AXEAiFormbuilderOutlineLayoutForAiCommand.execute
|
|
1750
|
+
*/
|
|
1751
|
+
const axeAiFormbuilderOutlineLayoutForAiCommandDefinition = defineCommand({
|
|
1752
|
+
key: 'AiFormbuilder:OutlineLayoutForAi',
|
|
1753
|
+
title: 'Outline a layout JSON tree',
|
|
1754
|
+
description: 'Returns a compact list of widgets (name, type, label, parent, path) from an AXPWidgetNode JSON tree. Does not touch the live designer. Pass the same json to Add/Update/Remove/Move.',
|
|
1755
|
+
executionMode: 'frontend',
|
|
1756
|
+
inputs: [
|
|
1757
|
+
{
|
|
1758
|
+
name: 'json',
|
|
1759
|
+
title: 'Layout JSON',
|
|
1760
|
+
description: 'AXPWidgetNode tree (string or object). Chat block-layout or designer document-layout.',
|
|
1761
|
+
interface: { name: 'large-text-editor' },
|
|
1762
|
+
validations: [{ rule: 'required' }],
|
|
1763
|
+
},
|
|
1764
|
+
],
|
|
1765
|
+
outcomes: {
|
|
1766
|
+
mode: 'static',
|
|
1767
|
+
values: ['success', 'failure'],
|
|
1768
|
+
},
|
|
1769
|
+
ai: {
|
|
1770
|
+
shortDescription: 'List widgets in a layout JSON tree by name/type/path without rewriting the tree.',
|
|
1771
|
+
tags: ['ai-formbuilder', 'layout', 'ai', 'outline', 'json'],
|
|
1772
|
+
},
|
|
1773
|
+
capabilities: ['ai'],
|
|
1774
|
+
categories: [AXP_COMMAND_DEFINITION_CATEGORY_AI_TOOLS],
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
//#endregion
|
|
1778
|
+
//#region ----- Command -----
|
|
1779
|
+
class AXEAiFormbuilderAddWidgetForAiCommand {
|
|
1780
|
+
async execute(input) {
|
|
1781
|
+
const data = axeAddWidgetToLayoutJson(input);
|
|
1782
|
+
return {
|
|
1783
|
+
success: true,
|
|
1784
|
+
kind: 'object',
|
|
1785
|
+
data,
|
|
1786
|
+
...(data.applied ? {} : { message: { text: data.errors[0] ?? 'The widget was not added.' } }),
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderAddWidgetForAiCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1790
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderAddWidgetForAiCommand, providedIn: 'root' }); }
|
|
1791
|
+
}
|
|
1792
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderAddWidgetForAiCommand, decorators: [{
|
|
1793
|
+
type: Injectable,
|
|
1794
|
+
args: [{ providedIn: 'root' }]
|
|
1795
|
+
}] });
|
|
1796
|
+
|
|
1797
|
+
var addWidgetForAi_command = /*#__PURE__*/Object.freeze({
|
|
1798
|
+
__proto__: null,
|
|
1799
|
+
AXEAiFormbuilderAddWidgetForAiCommand: AXEAiFormbuilderAddWidgetForAiCommand
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* Build-time only. Do not import from Angular modules.
|
|
1804
|
+
* Source: add-widget-for-ai.command.ts — AXEAiFormbuilderAddWidgetForAiCommand.execute
|
|
1805
|
+
*/
|
|
1806
|
+
const axeAiFormbuilderAddWidgetForAiCommandDefinition = defineCommand({
|
|
1807
|
+
key: 'AiFormbuilder:AddWidgetForAi',
|
|
1808
|
+
title: 'Add a widget in layout JSON',
|
|
1809
|
+
description: 'Inserts one AXPWidgetNode under a parent inside a layout JSON tree and returns the updated json. Does not touch the live designer. Address the parent by name or path from OutlineLayoutForAi.',
|
|
1810
|
+
executionMode: 'frontend',
|
|
1811
|
+
inputs: [
|
|
1812
|
+
{
|
|
1813
|
+
name: 'json',
|
|
1814
|
+
title: 'Layout JSON',
|
|
1815
|
+
description: 'Current AXPWidgetNode tree (string or object).',
|
|
1816
|
+
interface: { name: 'large-text-editor' },
|
|
1817
|
+
validations: [{ rule: 'required' }],
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
name: 'parentName',
|
|
1821
|
+
title: 'Parent widget name',
|
|
1822
|
+
description: 'name from OutlineLayoutForAi. Prefer this over parentPath.',
|
|
1823
|
+
interface: { name: 'text-editor' },
|
|
1824
|
+
},
|
|
1825
|
+
{
|
|
1826
|
+
name: 'parentPath',
|
|
1827
|
+
title: 'Parent widget path',
|
|
1828
|
+
description: 'path from OutlineLayoutForAi when the parent has no name.',
|
|
1829
|
+
interface: { name: 'text-editor' },
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
name: 'widget',
|
|
1833
|
+
title: 'Widget node',
|
|
1834
|
+
description: 'One AXPWidgetNode object (type plus options/children). Not the full tree.',
|
|
1835
|
+
interface: { name: 'json' },
|
|
1836
|
+
validations: [{ rule: 'required' }],
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
name: 'index',
|
|
1840
|
+
title: 'Insert index',
|
|
1841
|
+
description: 'Optional child index under the parent. Omit to append.',
|
|
1842
|
+
interface: { name: 'number-editor' },
|
|
1843
|
+
},
|
|
1844
|
+
],
|
|
1845
|
+
outcomes: {
|
|
1846
|
+
mode: 'static',
|
|
1847
|
+
values: ['success', 'failure'],
|
|
1848
|
+
},
|
|
1849
|
+
ai: {
|
|
1850
|
+
shortDescription: 'Add one widget under a named parent in a layout JSON tree and return the new json.',
|
|
1851
|
+
tags: ['ai-formbuilder', 'layout', 'ai', 'add', 'json'],
|
|
1852
|
+
},
|
|
1853
|
+
capabilities: ['ai'],
|
|
1854
|
+
categories: [AXP_COMMAND_DEFINITION_CATEGORY_AI_TOOLS],
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
//#endregion
|
|
1858
|
+
//#region ----- Command -----
|
|
1859
|
+
class AXEAiFormbuilderUpdateWidgetForAiCommand {
|
|
1860
|
+
async execute(input) {
|
|
1861
|
+
const data = axeUpdateWidgetInLayoutJson(input);
|
|
1862
|
+
return {
|
|
1863
|
+
success: true,
|
|
1864
|
+
kind: 'object',
|
|
1865
|
+
data,
|
|
1866
|
+
...(data.applied ? {} : { message: { text: data.errors[0] ?? 'The widget was not updated.' } }),
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderUpdateWidgetForAiCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1870
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderUpdateWidgetForAiCommand, providedIn: 'root' }); }
|
|
1871
|
+
}
|
|
1872
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderUpdateWidgetForAiCommand, decorators: [{
|
|
1873
|
+
type: Injectable,
|
|
1874
|
+
args: [{ providedIn: 'root' }]
|
|
1875
|
+
}] });
|
|
1876
|
+
|
|
1877
|
+
var updateWidgetForAi_command = /*#__PURE__*/Object.freeze({
|
|
1878
|
+
__proto__: null,
|
|
1879
|
+
AXEAiFormbuilderUpdateWidgetForAiCommand: AXEAiFormbuilderUpdateWidgetForAiCommand
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
/**
|
|
1883
|
+
* Build-time only. Do not import from Angular modules.
|
|
1884
|
+
* Source: update-widget-for-ai.command.ts — AXEAiFormbuilderUpdateWidgetForAiCommand.execute
|
|
1885
|
+
*/
|
|
1886
|
+
const axeAiFormbuilderUpdateWidgetForAiCommandDefinition = defineCommand({
|
|
1887
|
+
key: 'AiFormbuilder:UpdateWidgetForAi',
|
|
1888
|
+
title: 'Update a widget in layout JSON',
|
|
1889
|
+
description: 'Patches options, validations, triggers, and/or visible on one widget inside a layout JSON tree and returns the updated json. triggers and visible sit on the node root, not inside options. Does not touch the live designer.',
|
|
1890
|
+
executionMode: 'frontend',
|
|
1891
|
+
inputs: [
|
|
1892
|
+
{
|
|
1893
|
+
name: 'json',
|
|
1894
|
+
title: 'Layout JSON',
|
|
1895
|
+
description: 'Current AXPWidgetNode tree (string or object).',
|
|
1896
|
+
interface: { name: 'large-text-editor' },
|
|
1897
|
+
validations: [{ rule: 'required' }],
|
|
1898
|
+
},
|
|
1899
|
+
{
|
|
1900
|
+
name: 'name',
|
|
1901
|
+
title: 'Widget name',
|
|
1902
|
+
description: 'name from OutlineLayoutForAi. Prefer this over path.',
|
|
1903
|
+
interface: { name: 'text-editor' },
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
name: 'path',
|
|
1907
|
+
title: 'Widget path',
|
|
1908
|
+
description: 'path from OutlineLayoutForAi when the widget has no name.',
|
|
1909
|
+
interface: { name: 'text-editor' },
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
name: 'options',
|
|
1913
|
+
title: 'Options patch',
|
|
1914
|
+
description: 'Partial options object merged onto the widget (label, hint, cols, …).',
|
|
1915
|
+
interface: { name: 'json' },
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
name: 'validations',
|
|
1919
|
+
title: 'Validations',
|
|
1920
|
+
description: 'Replaces options.validations on the widget (e.g. [{ "rule": "required" }]).',
|
|
1921
|
+
interface: { name: 'json' },
|
|
1922
|
+
},
|
|
1923
|
+
{
|
|
1924
|
+
name: 'triggers',
|
|
1925
|
+
title: 'Triggers',
|
|
1926
|
+
description: 'Replaces node.triggers: [{ event, action }] expression strings. Empty array clears.',
|
|
1927
|
+
interface: { name: 'json' },
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
name: 'visible',
|
|
1931
|
+
title: 'Visible',
|
|
1932
|
+
description: 'Replaces node.visible (boolean or "{{ context.eval(...) }}").',
|
|
1933
|
+
interface: { name: 'text-editor' },
|
|
1934
|
+
},
|
|
1935
|
+
],
|
|
1936
|
+
outcomes: {
|
|
1937
|
+
mode: 'static',
|
|
1938
|
+
values: ['success', 'failure'],
|
|
1939
|
+
},
|
|
1940
|
+
ai: {
|
|
1941
|
+
shortDescription: 'Patch options, validations, triggers, or visible on one widget in a layout JSON tree.',
|
|
1942
|
+
tags: ['ai-formbuilder', 'layout', 'ai', 'update', 'json'],
|
|
1943
|
+
},
|
|
1944
|
+
capabilities: ['ai'],
|
|
1945
|
+
categories: [AXP_COMMAND_DEFINITION_CATEGORY_AI_TOOLS],
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
//#endregion
|
|
1949
|
+
//#region ----- Command -----
|
|
1950
|
+
class AXEAiFormbuilderRemoveWidgetForAiCommand {
|
|
1951
|
+
async execute(input) {
|
|
1952
|
+
const data = axeRemoveWidgetFromLayoutJson(input);
|
|
1953
|
+
return {
|
|
1954
|
+
success: true,
|
|
1955
|
+
kind: 'object',
|
|
1956
|
+
data,
|
|
1957
|
+
...(data.applied ? {} : { message: { text: data.errors[0] ?? 'The widget was not removed.' } }),
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderRemoveWidgetForAiCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1961
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderRemoveWidgetForAiCommand, providedIn: 'root' }); }
|
|
1962
|
+
}
|
|
1963
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderRemoveWidgetForAiCommand, decorators: [{
|
|
1964
|
+
type: Injectable,
|
|
1965
|
+
args: [{ providedIn: 'root' }]
|
|
1966
|
+
}] });
|
|
1967
|
+
|
|
1968
|
+
var removeWidgetForAi_command = /*#__PURE__*/Object.freeze({
|
|
1969
|
+
__proto__: null,
|
|
1970
|
+
AXEAiFormbuilderRemoveWidgetForAiCommand: AXEAiFormbuilderRemoveWidgetForAiCommand
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* Build-time only. Do not import from Angular modules.
|
|
1975
|
+
* Source: remove-widget-for-ai.command.ts — AXEAiFormbuilderRemoveWidgetForAiCommand.execute
|
|
1976
|
+
*/
|
|
1977
|
+
const axeAiFormbuilderRemoveWidgetForAiCommandDefinition = defineCommand({
|
|
1978
|
+
key: 'AiFormbuilder:RemoveWidgetForAi',
|
|
1979
|
+
title: 'Remove a widget from layout JSON',
|
|
1980
|
+
description: 'Removes one widget from a layout JSON tree and returns the updated json. Does not touch the live designer. Cannot remove the root. Address the widget by name or path from OutlineLayoutForAi.',
|
|
1981
|
+
executionMode: 'frontend',
|
|
1982
|
+
inputs: [
|
|
1983
|
+
{
|
|
1984
|
+
name: 'json',
|
|
1985
|
+
title: 'Layout JSON',
|
|
1986
|
+
description: 'Current AXPWidgetNode tree (string or object).',
|
|
1987
|
+
interface: { name: 'large-text-editor' },
|
|
1988
|
+
validations: [{ rule: 'required' }],
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
name: 'name',
|
|
1992
|
+
title: 'Widget name',
|
|
1993
|
+
description: 'name from OutlineLayoutForAi. Prefer this over path.',
|
|
1994
|
+
interface: { name: 'text-editor' },
|
|
1995
|
+
},
|
|
1996
|
+
{
|
|
1997
|
+
name: 'path',
|
|
1998
|
+
title: 'Widget path',
|
|
1999
|
+
description: 'path from OutlineLayoutForAi when the widget has no name.',
|
|
2000
|
+
interface: { name: 'text-editor' },
|
|
2001
|
+
},
|
|
2002
|
+
],
|
|
2003
|
+
outcomes: {
|
|
2004
|
+
mode: 'static',
|
|
2005
|
+
values: ['success', 'failure'],
|
|
2006
|
+
},
|
|
2007
|
+
ai: {
|
|
2008
|
+
shortDescription: 'Remove one widget from a layout JSON tree and return the new json.',
|
|
2009
|
+
tags: ['ai-formbuilder', 'layout', 'ai', 'remove', 'json'],
|
|
2010
|
+
},
|
|
2011
|
+
capabilities: ['ai'],
|
|
2012
|
+
categories: [AXP_COMMAND_DEFINITION_CATEGORY_AI_TOOLS],
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
//#endregion
|
|
2016
|
+
//#region ----- Command -----
|
|
2017
|
+
class AXEAiFormbuilderMoveWidgetForAiCommand {
|
|
2018
|
+
async execute(input) {
|
|
2019
|
+
const data = axeMoveWidgetInLayoutJson(input);
|
|
2020
|
+
return {
|
|
2021
|
+
success: true,
|
|
2022
|
+
kind: 'object',
|
|
2023
|
+
data,
|
|
2024
|
+
...(data.applied ? {} : { message: { text: data.errors[0] ?? 'The widget was not moved.' } }),
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderMoveWidgetForAiCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2028
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderMoveWidgetForAiCommand, providedIn: 'root' }); }
|
|
2029
|
+
}
|
|
2030
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiFormbuilderMoveWidgetForAiCommand, decorators: [{
|
|
2031
|
+
type: Injectable,
|
|
2032
|
+
args: [{ providedIn: 'root' }]
|
|
2033
|
+
}] });
|
|
2034
|
+
|
|
2035
|
+
var moveWidgetForAi_command = /*#__PURE__*/Object.freeze({
|
|
2036
|
+
__proto__: null,
|
|
2037
|
+
AXEAiFormbuilderMoveWidgetForAiCommand: AXEAiFormbuilderMoveWidgetForAiCommand
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
/**
|
|
2041
|
+
* Build-time only. Do not import from Angular modules.
|
|
2042
|
+
* Source: move-widget-for-ai.command.ts — AXEAiFormbuilderMoveWidgetForAiCommand.execute
|
|
2043
|
+
*/
|
|
2044
|
+
const axeAiFormbuilderMoveWidgetForAiCommandDefinition = defineCommand({
|
|
2045
|
+
key: 'AiFormbuilder:MoveWidgetForAi',
|
|
2046
|
+
title: 'Move a widget in layout JSON',
|
|
2047
|
+
description: 'Moves one widget to another parent and/or index inside a layout JSON tree and returns the updated json. Does not touch the live designer. Address widgets by name or path from OutlineLayoutForAi.',
|
|
2048
|
+
executionMode: 'frontend',
|
|
2049
|
+
inputs: [
|
|
2050
|
+
{
|
|
2051
|
+
name: 'json',
|
|
2052
|
+
title: 'Layout JSON',
|
|
2053
|
+
description: 'Current AXPWidgetNode tree (string or object).',
|
|
2054
|
+
interface: { name: 'large-text-editor' },
|
|
2055
|
+
validations: [{ rule: 'required' }],
|
|
2056
|
+
},
|
|
2057
|
+
{
|
|
2058
|
+
name: 'name',
|
|
2059
|
+
title: 'Widget name',
|
|
2060
|
+
description: 'name from OutlineLayoutForAi. Prefer this over path.',
|
|
2061
|
+
interface: { name: 'text-editor' },
|
|
2062
|
+
},
|
|
2063
|
+
{
|
|
2064
|
+
name: 'path',
|
|
2065
|
+
title: 'Widget path',
|
|
2066
|
+
description: 'path from OutlineLayoutForAi when the widget has no name.',
|
|
2067
|
+
interface: { name: 'text-editor' },
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
name: 'parentName',
|
|
2071
|
+
title: 'New parent name',
|
|
2072
|
+
description: 'Destination parent name from OutlineLayoutForAi. Omit to reorder under the current parent.',
|
|
2073
|
+
interface: { name: 'text-editor' },
|
|
2074
|
+
},
|
|
2075
|
+
{
|
|
2076
|
+
name: 'parentPath',
|
|
2077
|
+
title: 'New parent path',
|
|
2078
|
+
description: 'Destination parent path when the parent has no name.',
|
|
2079
|
+
interface: { name: 'text-editor' },
|
|
2080
|
+
},
|
|
2081
|
+
{
|
|
2082
|
+
name: 'index',
|
|
2083
|
+
title: 'Insert index',
|
|
2084
|
+
description: 'Child index under the destination parent.',
|
|
2085
|
+
interface: { name: 'number-editor' },
|
|
2086
|
+
},
|
|
2087
|
+
],
|
|
2088
|
+
outcomes: {
|
|
2089
|
+
mode: 'static',
|
|
2090
|
+
values: ['success', 'failure'],
|
|
2091
|
+
},
|
|
2092
|
+
ai: {
|
|
2093
|
+
shortDescription: 'Move one widget in a layout JSON tree and return the new json.',
|
|
2094
|
+
tags: ['ai-formbuilder', 'layout', 'ai', 'move', 'json'],
|
|
315
2095
|
},
|
|
316
2096
|
capabilities: ['ai'],
|
|
317
2097
|
categories: [AXP_COMMAND_DEFINITION_CATEGORY_AI_TOOLS],
|
|
@@ -321,5 +2101,5 @@ const axeAiFormbuilderValidateLayoutJsonForAiCommandDefinition = defineCommand({
|
|
|
321
2101
|
* Generated bundle index. Do not edit.
|
|
322
2102
|
*/
|
|
323
2103
|
|
|
324
|
-
export { AXEAIFormBuilderExtensionModule, AXEAiFormbuilderDesignerAiAssistService,
|
|
2104
|
+
export { AXEAIFormBuilderExtensionModule, AXEAiFormbuilderAddWidgetForAiCommand, AXEAiFormbuilderApplyLayoutJsonForAiCommand, AXEAiFormbuilderDesignerAiAssistService, AXEAiFormbuilderDesignerFooterHookProvider, AXEAiFormbuilderDesignerHelpMenuHookProvider, AXEAiFormbuilderMoveWidgetForAiCommand, AXEAiFormbuilderOutlineLayoutForAiCommand, AXEAiFormbuilderRemoveWidgetForAiCommand, AXEAiFormbuilderUpdateWidgetForAiCommand, AXEAiFormbuilderValidateLayoutJsonForAiCommand, AXE_AI_FORMBUILDER_ADD_WIDGET_COMMAND, AXE_AI_FORMBUILDER_APPLY_LAYOUT_COMMAND, AXE_AI_FORMBUILDER_ASSIST_ACTION, AXE_AI_FORMBUILDER_ASSIST_KIND, AXE_AI_FORMBUILDER_ASSIST_SCENARIO, AXE_AI_FORMBUILDER_DESIGNER_ASSIST_ID, AXE_AI_FORMBUILDER_DESIGNER_DRAFT_ENTITY_ID, AXE_AI_FORMBUILDER_DESIGNER_ENTITY_TYPE, AXE_AI_FORMBUILDER_MOVE_WIDGET_COMMAND, AXE_AI_FORMBUILDER_OUTLINE_LAYOUT_COMMAND, AXE_AI_FORMBUILDER_PAGE_ACTION_ADD_WIDGET, AXE_AI_FORMBUILDER_PAGE_ACTION_APPLY_LAYOUT, AXE_AI_FORMBUILDER_PAGE_ACTION_MOVE_WIDGET, AXE_AI_FORMBUILDER_PAGE_ACTION_REMOVE_WIDGET, AXE_AI_FORMBUILDER_PAGE_ACTION_UPDATE_WIDGET, AXE_AI_FORMBUILDER_PAGE_CONTEXT_ID, AXE_AI_FORMBUILDER_REMOVE_WIDGET_COMMAND, AXE_AI_FORMBUILDER_UPDATE_WIDGET_COMMAND, AXE_AI_FORMBUILDER_VALIDATE_LAYOUT_COMMAND, axeAddWidgetToLayoutJson, axeAiFormbuilderAddWidgetForAiCommandDefinition, axeAiFormbuilderApplyLayoutJsonForAiCommandDefinition, axeAiFormbuilderMoveWidgetForAiCommandDefinition, axeAiFormbuilderOutlineLayoutForAiCommandDefinition, axeAiFormbuilderRemoveWidgetForAiCommandDefinition, axeAiFormbuilderUpdateWidgetForAiCommandDefinition, axeAiFormbuilderValidateLayoutJsonForAiCommandDefinition, axeMoveWidgetInLayoutJson, axeOutlineLayoutJson, axeRemoveWidgetFromLayoutJson, axeUpdateWidgetInLayoutJson };
|
|
325
2105
|
//# sourceMappingURL=acorex-platform-extension-ai-formbuilder-client.mjs.map
|