@elizaos/plugin-form 2.0.0-alpha.10 → 2.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4B5QLNVA.js +187 -0
- package/dist/chunk-ARWZY3NX.js +284 -0
- package/dist/chunk-R4VBS2YK.js +1597 -0
- package/dist/chunk-TBCL2ILB.js +172 -0
- package/dist/chunk-WY4WK3HD.js +57 -0
- package/dist/chunk-XHECCAUT.js +544 -0
- package/dist/chunk-YTWANJ3R.js +64 -0
- package/dist/context-MHPFYZZ2.js +9 -0
- package/dist/extractor-UWASKXKD.js +11 -0
- package/dist/index.d.ts +3213 -2
- package/dist/index.js +428 -2829
- package/dist/restore-S7JLME4H.js +9 -0
- package/dist/service-TCCXKV3T.js +7 -0
- package/package.json +13 -11
- package/dist/index.js.map +0 -30
package/dist/index.js
CHANGED
|
@@ -1,2661 +1,64 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __export = (target, all) => {
|
|
3
|
-
for (var name in all)
|
|
4
|
-
__defProp(target, name, {
|
|
5
|
-
get: all[name],
|
|
6
|
-
enumerable: true,
|
|
7
|
-
configurable: true,
|
|
8
|
-
set: (newValue) => all[name] = () => newValue
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
12
|
-
|
|
13
|
-
// src/types.ts
|
|
14
|
-
var FORM_CONTROL_DEFAULTS, FORM_DEFINITION_DEFAULTS, FORM_SESSION_COMPONENT = "form_session", FORM_SUBMISSION_COMPONENT = "form_submission", FORM_AUTOFILL_COMPONENT = "form_autofill";
|
|
15
|
-
var init_types = __esm(() => {
|
|
16
|
-
FORM_CONTROL_DEFAULTS = {
|
|
17
|
-
type: "text",
|
|
18
|
-
required: false,
|
|
19
|
-
confirmThreshold: 0.8
|
|
20
|
-
};
|
|
21
|
-
FORM_DEFINITION_DEFAULTS = {
|
|
22
|
-
version: 1,
|
|
23
|
-
status: "active",
|
|
24
|
-
ux: {
|
|
25
|
-
allowUndo: true,
|
|
26
|
-
allowSkip: true,
|
|
27
|
-
maxUndoSteps: 5,
|
|
28
|
-
showExamples: true,
|
|
29
|
-
showExplanations: true,
|
|
30
|
-
allowAutofill: true
|
|
31
|
-
},
|
|
32
|
-
ttl: {
|
|
33
|
-
minDays: 14,
|
|
34
|
-
maxDays: 90,
|
|
35
|
-
effortMultiplier: 0.5
|
|
36
|
-
},
|
|
37
|
-
nudge: {
|
|
38
|
-
enabled: true,
|
|
39
|
-
afterInactiveHours: 48,
|
|
40
|
-
maxNudges: 3
|
|
41
|
-
},
|
|
42
|
-
debug: false
|
|
43
|
-
};
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// src/builtins.ts
|
|
47
|
-
function registerBuiltinTypes(registerFn) {
|
|
48
|
-
for (const type of BUILTIN_TYPES) {
|
|
49
|
-
registerFn(type);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function getBuiltinType(id) {
|
|
53
|
-
return BUILTIN_TYPE_MAP.get(id);
|
|
54
|
-
}
|
|
55
|
-
function isBuiltinType(id) {
|
|
56
|
-
return BUILTIN_TYPE_MAP.has(id);
|
|
57
|
-
}
|
|
58
|
-
var EMAIL_REGEX, ISO_DATE_REGEX, textType, numberType, emailType, booleanType, selectType, dateType, fileType, BUILTIN_TYPES, BUILTIN_TYPE_MAP;
|
|
59
|
-
var init_builtins = __esm(() => {
|
|
60
|
-
EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
61
|
-
ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
62
|
-
textType = {
|
|
63
|
-
id: "text",
|
|
64
|
-
builtin: true,
|
|
65
|
-
validate: (value, control) => {
|
|
66
|
-
if (value === null || value === undefined) {
|
|
67
|
-
return { valid: true };
|
|
68
|
-
}
|
|
69
|
-
const str = String(value);
|
|
70
|
-
if (control.minLength !== undefined && str.length < control.minLength) {
|
|
71
|
-
return {
|
|
72
|
-
valid: false,
|
|
73
|
-
error: `Must be at least ${control.minLength} characters`
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
if (control.maxLength !== undefined && str.length > control.maxLength) {
|
|
77
|
-
return {
|
|
78
|
-
valid: false,
|
|
79
|
-
error: `Must be at most ${control.maxLength} characters`
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
if (control.pattern) {
|
|
83
|
-
const regex = new RegExp(control.pattern);
|
|
84
|
-
if (!regex.test(str)) {
|
|
85
|
-
return { valid: false, error: "Invalid format" };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (control.enum && !control.enum.includes(str)) {
|
|
89
|
-
return {
|
|
90
|
-
valid: false,
|
|
91
|
-
error: `Must be one of: ${control.enum.join(", ")}`
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
return { valid: true };
|
|
95
|
-
},
|
|
96
|
-
parse: (value) => String(value).trim(),
|
|
97
|
-
format: (value) => String(value ?? ""),
|
|
98
|
-
extractionPrompt: "a text string"
|
|
99
|
-
};
|
|
100
|
-
numberType = {
|
|
101
|
-
id: "number",
|
|
102
|
-
builtin: true,
|
|
103
|
-
validate: (value, control) => {
|
|
104
|
-
if (value === null || value === undefined || value === "") {
|
|
105
|
-
return { valid: true };
|
|
106
|
-
}
|
|
107
|
-
const num = typeof value === "number" ? value : parseFloat(String(value));
|
|
108
|
-
if (Number.isNaN(num)) {
|
|
109
|
-
return { valid: false, error: "Must be a valid number" };
|
|
110
|
-
}
|
|
111
|
-
if (control.min !== undefined && num < control.min) {
|
|
112
|
-
return { valid: false, error: `Must be at least ${control.min}` };
|
|
113
|
-
}
|
|
114
|
-
if (control.max !== undefined && num > control.max) {
|
|
115
|
-
return { valid: false, error: `Must be at most ${control.max}` };
|
|
116
|
-
}
|
|
117
|
-
return { valid: true };
|
|
118
|
-
},
|
|
119
|
-
parse: (value) => {
|
|
120
|
-
const cleaned = value.replace(/[,$\s]/g, "");
|
|
121
|
-
return parseFloat(cleaned);
|
|
122
|
-
},
|
|
123
|
-
format: (value) => {
|
|
124
|
-
if (value === null || value === undefined)
|
|
125
|
-
return "";
|
|
126
|
-
const num = typeof value === "number" ? value : parseFloat(String(value));
|
|
127
|
-
if (Number.isNaN(num))
|
|
128
|
-
return String(value);
|
|
129
|
-
return num.toLocaleString();
|
|
130
|
-
},
|
|
131
|
-
extractionPrompt: "a number (integer or decimal)"
|
|
132
|
-
};
|
|
133
|
-
emailType = {
|
|
134
|
-
id: "email",
|
|
135
|
-
builtin: true,
|
|
136
|
-
validate: (value) => {
|
|
137
|
-
if (value === null || value === undefined || value === "") {
|
|
138
|
-
return { valid: true };
|
|
139
|
-
}
|
|
140
|
-
const str = String(value).trim().toLowerCase();
|
|
141
|
-
if (!EMAIL_REGEX.test(str)) {
|
|
142
|
-
return { valid: false, error: "Invalid email format" };
|
|
143
|
-
}
|
|
144
|
-
return { valid: true };
|
|
145
|
-
},
|
|
146
|
-
parse: (value) => value.trim().toLowerCase(),
|
|
147
|
-
format: (value) => String(value ?? "").toLowerCase(),
|
|
148
|
-
extractionPrompt: "an email address (e.g., user@example.com)"
|
|
149
|
-
};
|
|
150
|
-
booleanType = {
|
|
151
|
-
id: "boolean",
|
|
152
|
-
builtin: true,
|
|
153
|
-
validate: (value) => {
|
|
154
|
-
if (value === null || value === undefined) {
|
|
155
|
-
return { valid: true };
|
|
156
|
-
}
|
|
157
|
-
if (typeof value === "boolean") {
|
|
158
|
-
return { valid: true };
|
|
159
|
-
}
|
|
160
|
-
const str = String(value).toLowerCase();
|
|
161
|
-
const validValues = ["true", "false", "yes", "no", "1", "0", "on", "off"];
|
|
162
|
-
if (!validValues.includes(str)) {
|
|
163
|
-
return { valid: false, error: "Must be yes/no or true/false" };
|
|
164
|
-
}
|
|
165
|
-
return { valid: true };
|
|
166
|
-
},
|
|
167
|
-
parse: (value) => {
|
|
168
|
-
const str = value.toLowerCase();
|
|
169
|
-
return ["true", "yes", "1", "on"].includes(str);
|
|
170
|
-
},
|
|
171
|
-
format: (value) => {
|
|
172
|
-
if (value === true)
|
|
173
|
-
return "Yes";
|
|
174
|
-
if (value === false)
|
|
175
|
-
return "No";
|
|
176
|
-
return String(value ?? "");
|
|
177
|
-
},
|
|
178
|
-
extractionPrompt: "a yes/no or true/false value"
|
|
179
|
-
};
|
|
180
|
-
selectType = {
|
|
181
|
-
id: "select",
|
|
182
|
-
builtin: true,
|
|
183
|
-
validate: (value, control) => {
|
|
184
|
-
if (value === null || value === undefined || value === "") {
|
|
185
|
-
return { valid: true };
|
|
186
|
-
}
|
|
187
|
-
const str = String(value);
|
|
188
|
-
if (control.options) {
|
|
189
|
-
const validValues = control.options.map((o) => o.value);
|
|
190
|
-
if (!validValues.includes(str)) {
|
|
191
|
-
const labels = control.options.map((o) => o.label).join(", ");
|
|
192
|
-
return { valid: false, error: `Must be one of: ${labels}` };
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (control.enum && !control.options) {
|
|
196
|
-
if (!control.enum.includes(str)) {
|
|
197
|
-
return {
|
|
198
|
-
valid: false,
|
|
199
|
-
error: `Must be one of: ${control.enum.join(", ")}`
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return { valid: true };
|
|
204
|
-
},
|
|
205
|
-
parse: (value) => value.trim(),
|
|
206
|
-
format: (value) => String(value ?? ""),
|
|
207
|
-
extractionPrompt: "one of the available options"
|
|
208
|
-
};
|
|
209
|
-
dateType = {
|
|
210
|
-
id: "date",
|
|
211
|
-
builtin: true,
|
|
212
|
-
validate: (value) => {
|
|
213
|
-
if (value === null || value === undefined || value === "") {
|
|
214
|
-
return { valid: true };
|
|
215
|
-
}
|
|
216
|
-
const str = String(value);
|
|
217
|
-
if (!ISO_DATE_REGEX.test(str)) {
|
|
218
|
-
return { valid: false, error: "Must be in YYYY-MM-DD format" };
|
|
219
|
-
}
|
|
220
|
-
const date = new Date(str);
|
|
221
|
-
if (Number.isNaN(date.getTime())) {
|
|
222
|
-
return { valid: false, error: "Invalid date" };
|
|
223
|
-
}
|
|
224
|
-
return { valid: true };
|
|
225
|
-
},
|
|
226
|
-
parse: (value) => {
|
|
227
|
-
const date = new Date(value);
|
|
228
|
-
if (!Number.isNaN(date.getTime())) {
|
|
229
|
-
return date.toISOString().split("T")[0];
|
|
230
|
-
}
|
|
231
|
-
return value.trim();
|
|
232
|
-
},
|
|
233
|
-
format: (value) => {
|
|
234
|
-
if (!value)
|
|
235
|
-
return "";
|
|
236
|
-
const date = new Date(String(value));
|
|
237
|
-
if (Number.isNaN(date.getTime()))
|
|
238
|
-
return String(value);
|
|
239
|
-
return date.toLocaleDateString();
|
|
240
|
-
},
|
|
241
|
-
extractionPrompt: "a date (preferably in YYYY-MM-DD format)"
|
|
242
|
-
};
|
|
243
|
-
fileType = {
|
|
244
|
-
id: "file",
|
|
245
|
-
builtin: true,
|
|
246
|
-
validate: (value, _control) => {
|
|
247
|
-
if (value === null || value === undefined) {
|
|
248
|
-
return { valid: true };
|
|
249
|
-
}
|
|
250
|
-
if (typeof value === "object") {
|
|
251
|
-
return { valid: true };
|
|
252
|
-
}
|
|
253
|
-
return { valid: false, error: "Invalid file data" };
|
|
254
|
-
},
|
|
255
|
-
format: (value) => {
|
|
256
|
-
if (!value)
|
|
257
|
-
return "";
|
|
258
|
-
if (Array.isArray(value)) {
|
|
259
|
-
return `${value.length} file(s)`;
|
|
260
|
-
}
|
|
261
|
-
if (typeof value === "object" && value !== null && "name" in value) {
|
|
262
|
-
return String(value.name);
|
|
263
|
-
}
|
|
264
|
-
return "File attached";
|
|
265
|
-
},
|
|
266
|
-
extractionPrompt: "a file attachment (upload required)"
|
|
267
|
-
};
|
|
268
|
-
BUILTIN_TYPES = [
|
|
269
|
-
textType,
|
|
270
|
-
numberType,
|
|
271
|
-
emailType,
|
|
272
|
-
booleanType,
|
|
273
|
-
selectType,
|
|
274
|
-
dateType,
|
|
275
|
-
fileType
|
|
276
|
-
];
|
|
277
|
-
BUILTIN_TYPE_MAP = new Map(BUILTIN_TYPES.map((t) => [t.id, t]));
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
// src/validation.ts
|
|
281
|
-
function registerTypeHandler(type, handler) {
|
|
282
|
-
typeHandlers.set(type, handler);
|
|
283
|
-
}
|
|
284
|
-
function getTypeHandler(type) {
|
|
285
|
-
return typeHandlers.get(type);
|
|
286
|
-
}
|
|
287
|
-
function clearTypeHandlers() {
|
|
288
|
-
typeHandlers.clear();
|
|
289
|
-
}
|
|
290
|
-
function validateField(value, control) {
|
|
291
|
-
if (control.required) {
|
|
292
|
-
if (value === undefined || value === null || value === "") {
|
|
293
|
-
return {
|
|
294
|
-
valid: false,
|
|
295
|
-
error: `${control.label || control.key} is required`
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
if (value === undefined || value === null || value === "") {
|
|
300
|
-
return { valid: true };
|
|
301
|
-
}
|
|
302
|
-
const handler = typeHandlers.get(control.type);
|
|
303
|
-
if (handler?.validate) {
|
|
304
|
-
const result = handler.validate(value, control);
|
|
305
|
-
if (!result.valid) {
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
switch (control.type) {
|
|
310
|
-
case "email":
|
|
311
|
-
return validateEmail(value, control);
|
|
312
|
-
case "number":
|
|
313
|
-
return validateNumber(value, control);
|
|
314
|
-
case "boolean":
|
|
315
|
-
return validateBoolean(value, control);
|
|
316
|
-
case "date":
|
|
317
|
-
return validateDate(value, control);
|
|
318
|
-
case "select":
|
|
319
|
-
return validateSelect(value, control);
|
|
320
|
-
case "file":
|
|
321
|
-
return validateFile(value, control);
|
|
322
|
-
default:
|
|
323
|
-
return validateText(value, control);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
function validateText(value, control) {
|
|
327
|
-
const strValue = String(value);
|
|
328
|
-
if (control.pattern) {
|
|
329
|
-
const regex = new RegExp(control.pattern);
|
|
330
|
-
if (!regex.test(strValue)) {
|
|
331
|
-
return {
|
|
332
|
-
valid: false,
|
|
333
|
-
error: `${control.label || control.key} has invalid format`
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if (control.minLength !== undefined && strValue.length < control.minLength) {
|
|
338
|
-
return {
|
|
339
|
-
valid: false,
|
|
340
|
-
error: `${control.label || control.key} must be at least ${control.minLength} characters`
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
if (control.maxLength !== undefined && strValue.length > control.maxLength) {
|
|
344
|
-
return {
|
|
345
|
-
valid: false,
|
|
346
|
-
error: `${control.label || control.key} must be at most ${control.maxLength} characters`
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
if (control.enum && control.enum.length > 0) {
|
|
350
|
-
if (!control.enum.includes(strValue)) {
|
|
351
|
-
return {
|
|
352
|
-
valid: false,
|
|
353
|
-
error: `${control.label || control.key} must be one of: ${control.enum.join(", ")}`
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return { valid: true };
|
|
358
|
-
}
|
|
359
|
-
function validateEmail(value, control) {
|
|
360
|
-
const strValue = String(value);
|
|
361
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
362
|
-
if (!emailRegex.test(strValue)) {
|
|
363
|
-
return {
|
|
364
|
-
valid: false,
|
|
365
|
-
error: `${control.label || control.key} must be a valid email address`
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
return validateText(value, control);
|
|
369
|
-
}
|
|
370
|
-
function validateNumber(value, control) {
|
|
371
|
-
const numValue = typeof value === "number" ? value : parseFloat(String(value).replace(/[,$]/g, ""));
|
|
372
|
-
if (Number.isNaN(numValue)) {
|
|
373
|
-
return {
|
|
374
|
-
valid: false,
|
|
375
|
-
error: `${control.label || control.key} must be a number`
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
if (control.min !== undefined && numValue < control.min) {
|
|
379
|
-
return {
|
|
380
|
-
valid: false,
|
|
381
|
-
error: `${control.label || control.key} must be at least ${control.min}`
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
if (control.max !== undefined && numValue > control.max) {
|
|
385
|
-
return {
|
|
386
|
-
valid: false,
|
|
387
|
-
error: `${control.label || control.key} must be at most ${control.max}`
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
return { valid: true };
|
|
391
|
-
}
|
|
392
|
-
function validateBoolean(value, _control) {
|
|
393
|
-
if (typeof value === "boolean") {
|
|
394
|
-
return { valid: true };
|
|
395
|
-
}
|
|
396
|
-
const strValue = String(value).toLowerCase();
|
|
397
|
-
const truthy = ["true", "yes", "1", "on"];
|
|
398
|
-
const falsy = ["false", "no", "0", "off"];
|
|
399
|
-
if (truthy.includes(strValue) || falsy.includes(strValue)) {
|
|
400
|
-
return { valid: true };
|
|
401
|
-
}
|
|
402
|
-
return { valid: false, error: "Must be true or false" };
|
|
403
|
-
}
|
|
404
|
-
function validateDate(value, control) {
|
|
405
|
-
let dateValue;
|
|
406
|
-
if (value instanceof Date) {
|
|
407
|
-
dateValue = value;
|
|
408
|
-
} else if (typeof value === "string" || typeof value === "number") {
|
|
409
|
-
dateValue = new Date(value);
|
|
410
|
-
} else {
|
|
411
|
-
return {
|
|
412
|
-
valid: false,
|
|
413
|
-
error: `${control.label || control.key} must be a valid date`
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
if (Number.isNaN(dateValue.getTime())) {
|
|
417
|
-
return {
|
|
418
|
-
valid: false,
|
|
419
|
-
error: `${control.label || control.key} must be a valid date`
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
if (control.min !== undefined && dateValue.getTime() < control.min) {
|
|
423
|
-
return {
|
|
424
|
-
valid: false,
|
|
425
|
-
error: `${control.label || control.key} is too early`
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
if (control.max !== undefined && dateValue.getTime() > control.max) {
|
|
429
|
-
return {
|
|
430
|
-
valid: false,
|
|
431
|
-
error: `${control.label || control.key} is too late`
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
return { valid: true };
|
|
435
|
-
}
|
|
436
|
-
function validateSelect(value, control) {
|
|
437
|
-
if (!control.options || control.options.length === 0) {
|
|
438
|
-
return { valid: true };
|
|
439
|
-
}
|
|
440
|
-
const strValue = String(value);
|
|
441
|
-
const validValues = control.options.map((opt) => opt.value);
|
|
442
|
-
if (!validValues.includes(strValue)) {
|
|
443
|
-
return {
|
|
444
|
-
valid: false,
|
|
445
|
-
error: `${control.label || control.key} must be one of the available options`
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
return { valid: true };
|
|
449
|
-
}
|
|
450
|
-
function validateFile(value, control) {
|
|
451
|
-
if (!control.file) {
|
|
452
|
-
return { valid: true };
|
|
453
|
-
}
|
|
454
|
-
const files = Array.isArray(value) ? value : [value];
|
|
455
|
-
if (control.file.maxFiles && files.length > control.file.maxFiles) {
|
|
456
|
-
return {
|
|
457
|
-
valid: false,
|
|
458
|
-
error: `Maximum ${control.file.maxFiles} files allowed`
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
for (const file of files) {
|
|
462
|
-
if (!file || typeof file !== "object")
|
|
463
|
-
continue;
|
|
464
|
-
const fileObj = file;
|
|
465
|
-
if (control.file.maxSize && fileObj.size && fileObj.size > control.file.maxSize) {
|
|
466
|
-
return {
|
|
467
|
-
valid: false,
|
|
468
|
-
error: `File size exceeds maximum of ${formatBytes(control.file.maxSize)}`
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
if (control.file.accept && fileObj.mimeType) {
|
|
472
|
-
const accepted = control.file.accept.some((pattern) => matchesMimeType(fileObj.mimeType, pattern));
|
|
473
|
-
if (!accepted) {
|
|
474
|
-
return {
|
|
475
|
-
valid: false,
|
|
476
|
-
error: `File type ${fileObj.mimeType} is not accepted`
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return { valid: true };
|
|
482
|
-
}
|
|
483
|
-
function matchesMimeType(mimeType, pattern) {
|
|
484
|
-
if (pattern === "*/*")
|
|
485
|
-
return true;
|
|
486
|
-
if (pattern.endsWith("/*")) {
|
|
487
|
-
const prefix = pattern.slice(0, -1);
|
|
488
|
-
return mimeType.startsWith(prefix);
|
|
489
|
-
}
|
|
490
|
-
return mimeType === pattern;
|
|
491
|
-
}
|
|
492
|
-
function formatBytes(bytes) {
|
|
493
|
-
if (bytes < 1024)
|
|
494
|
-
return `${bytes} B`;
|
|
495
|
-
if (bytes < 1024 * 1024)
|
|
496
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
497
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
498
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
499
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
500
|
-
}
|
|
501
|
-
function parseValue(value, control) {
|
|
502
|
-
const handler = typeHandlers.get(control.type);
|
|
503
|
-
if (handler?.parse) {
|
|
504
|
-
return handler.parse(value);
|
|
505
|
-
}
|
|
506
|
-
switch (control.type) {
|
|
507
|
-
case "number":
|
|
508
|
-
return parseFloat(value.replace(/[,$]/g, ""));
|
|
509
|
-
case "boolean": {
|
|
510
|
-
const lower = value.toLowerCase();
|
|
511
|
-
return ["true", "yes", "1", "on"].includes(lower);
|
|
512
|
-
}
|
|
513
|
-
case "date": {
|
|
514
|
-
const timestamp = Date.parse(value);
|
|
515
|
-
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : value;
|
|
516
|
-
}
|
|
517
|
-
default:
|
|
518
|
-
return value;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
function formatValue(value, control) {
|
|
522
|
-
if (value === undefined || value === null)
|
|
523
|
-
return "";
|
|
524
|
-
const handler = typeHandlers.get(control.type);
|
|
525
|
-
if (handler?.format) {
|
|
526
|
-
return handler.format(value);
|
|
527
|
-
}
|
|
528
|
-
if (control.sensitive) {
|
|
529
|
-
const strVal = String(value);
|
|
530
|
-
if (strVal.length > 8) {
|
|
531
|
-
return `${strVal.slice(0, 4)}...${strVal.slice(-4)}`;
|
|
532
|
-
}
|
|
533
|
-
return "****";
|
|
534
|
-
}
|
|
535
|
-
switch (control.type) {
|
|
536
|
-
case "number":
|
|
537
|
-
return typeof value === "number" ? value.toLocaleString() : String(value);
|
|
538
|
-
case "boolean":
|
|
539
|
-
return value ? "Yes" : "No";
|
|
540
|
-
case "date":
|
|
541
|
-
return value instanceof Date ? value.toLocaleDateString() : String(value);
|
|
542
|
-
case "select":
|
|
543
|
-
if (control.options) {
|
|
544
|
-
const option = control.options.find((opt) => opt.value === String(value));
|
|
545
|
-
if (option)
|
|
546
|
-
return option.label;
|
|
547
|
-
}
|
|
548
|
-
return String(value);
|
|
549
|
-
case "file":
|
|
550
|
-
if (Array.isArray(value)) {
|
|
551
|
-
return value.map((f) => f.name || "file").join(", ");
|
|
552
|
-
}
|
|
553
|
-
return value.name || "file";
|
|
554
|
-
default:
|
|
555
|
-
return String(value);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
var typeHandlers;
|
|
559
|
-
var init_validation = __esm(() => {
|
|
560
|
-
typeHandlers = new Map;
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
// src/intent.ts
|
|
564
|
-
function quickIntentDetect(text) {
|
|
565
|
-
const lower = text.toLowerCase().trim();
|
|
566
|
-
if (lower.length < 2) {
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
if (/\b(resume|continue|pick up where|go back to|get back to)\b/.test(lower)) {
|
|
570
|
-
return "restore";
|
|
571
|
-
}
|
|
572
|
-
if (/\b(submit|done|finish|send it|that'?s all|i'?m done|complete|all set)\b/.test(lower)) {
|
|
573
|
-
return "submit";
|
|
574
|
-
}
|
|
575
|
-
if (/\b(save|stash|later|hold on|pause|save for later|come back|save this)\b/.test(lower)) {
|
|
576
|
-
if (!/\b(save and submit|save and send)\b/.test(lower)) {
|
|
577
|
-
return "stash";
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
if (/\b(cancel|abort|nevermind|never mind|forget it|stop|quit|exit)\b/.test(lower)) {
|
|
581
|
-
return "cancel";
|
|
582
|
-
}
|
|
583
|
-
if (/\b(undo|go back|wait no|change that|oops|that'?s wrong|wrong|not right)\b/.test(lower)) {
|
|
584
|
-
return "undo";
|
|
585
|
-
}
|
|
586
|
-
if (/\b(skip|pass|don'?t know|next one|next|don'?t have|no idea)\b/.test(lower)) {
|
|
587
|
-
if (!/\bskip to\b/.test(lower)) {
|
|
588
|
-
return "skip";
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (/\b(why|what'?s that for|explain|what do you mean|what is|purpose|reason)\b\??$/i.test(lower)) {
|
|
592
|
-
return "explain";
|
|
593
|
-
}
|
|
594
|
-
if (/^why\??$/i.test(lower)) {
|
|
595
|
-
return "explain";
|
|
596
|
-
}
|
|
597
|
-
if (/\b(example|like what|show me|such as|for instance|sample)\b\??$/i.test(lower)) {
|
|
598
|
-
return "example";
|
|
599
|
-
}
|
|
600
|
-
if (/^(example|e\.?g\.?)\??$/i.test(lower)) {
|
|
601
|
-
return "example";
|
|
602
|
-
}
|
|
603
|
-
if (/\b(how far|how many left|progress|status|how much more|where are we)\b/.test(lower)) {
|
|
604
|
-
return "progress";
|
|
605
|
-
}
|
|
606
|
-
if (/\b(same as|last time|use my usual|like before|previous|from before)\b/.test(lower)) {
|
|
607
|
-
return "autofill";
|
|
608
|
-
}
|
|
609
|
-
return null;
|
|
610
|
-
}
|
|
611
|
-
function isLifecycleIntent(intent) {
|
|
612
|
-
return ["submit", "stash", "restore", "cancel"].includes(intent);
|
|
613
|
-
}
|
|
614
|
-
function isUXIntent(intent) {
|
|
615
|
-
return ["undo", "skip", "explain", "example", "progress", "autofill"].includes(intent);
|
|
616
|
-
}
|
|
617
|
-
function hasDataToExtract(intent) {
|
|
618
|
-
return intent === "fill_form" || intent === "other";
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// node_modules/uuid/dist-node/stringify.js
|
|
622
|
-
function unsafeStringify(arr, offset = 0) {
|
|
623
|
-
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
624
|
-
}
|
|
625
|
-
var byteToHex;
|
|
626
|
-
var init_stringify = __esm(() => {
|
|
627
|
-
byteToHex = [];
|
|
628
|
-
for (let i = 0;i < 256; ++i) {
|
|
629
|
-
byteToHex.push((i + 256).toString(16).slice(1));
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
// node_modules/uuid/dist-node/rng.js
|
|
634
|
-
import { randomFillSync } from "node:crypto";
|
|
635
|
-
function rng() {
|
|
636
|
-
if (poolPtr > rnds8Pool.length - 16) {
|
|
637
|
-
randomFillSync(rnds8Pool);
|
|
638
|
-
poolPtr = 0;
|
|
639
|
-
}
|
|
640
|
-
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
641
|
-
}
|
|
642
|
-
var rnds8Pool, poolPtr;
|
|
643
|
-
var init_rng = __esm(() => {
|
|
644
|
-
rnds8Pool = new Uint8Array(256);
|
|
645
|
-
poolPtr = rnds8Pool.length;
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
// node_modules/uuid/dist-node/native.js
|
|
649
|
-
import { randomUUID } from "node:crypto";
|
|
650
|
-
var native_default;
|
|
651
|
-
var init_native = __esm(() => {
|
|
652
|
-
native_default = { randomUUID };
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
// node_modules/uuid/dist-node/v4.js
|
|
656
|
-
function _v4(options, buf, offset) {
|
|
657
|
-
options = options || {};
|
|
658
|
-
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
659
|
-
if (rnds.length < 16) {
|
|
660
|
-
throw new Error("Random bytes length must be >= 16");
|
|
661
|
-
}
|
|
662
|
-
rnds[6] = rnds[6] & 15 | 64;
|
|
663
|
-
rnds[8] = rnds[8] & 63 | 128;
|
|
664
|
-
if (buf) {
|
|
665
|
-
offset = offset || 0;
|
|
666
|
-
if (offset < 0 || offset + 16 > buf.length) {
|
|
667
|
-
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
668
|
-
}
|
|
669
|
-
for (let i = 0;i < 16; ++i) {
|
|
670
|
-
buf[offset + i] = rnds[i];
|
|
671
|
-
}
|
|
672
|
-
return buf;
|
|
673
|
-
}
|
|
674
|
-
return unsafeStringify(rnds);
|
|
675
|
-
}
|
|
676
|
-
function v4(options, buf, offset) {
|
|
677
|
-
if (native_default.randomUUID && !buf && !options) {
|
|
678
|
-
return native_default.randomUUID();
|
|
679
|
-
}
|
|
680
|
-
return _v4(options, buf, offset);
|
|
681
|
-
}
|
|
682
|
-
var v4_default;
|
|
683
|
-
var init_v4 = __esm(() => {
|
|
684
|
-
init_native();
|
|
685
|
-
init_rng();
|
|
686
|
-
init_stringify();
|
|
687
|
-
v4_default = v4;
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
// node_modules/uuid/dist-node/index.js
|
|
691
|
-
var init_dist_node = __esm(() => {
|
|
692
|
-
init_v4();
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
// src/storage.ts
|
|
696
|
-
async function getActiveSession(runtime, entityId, roomId) {
|
|
697
|
-
const component = await runtime.getComponent(entityId, `${FORM_SESSION_COMPONENT}:${roomId}`);
|
|
698
|
-
if (!component?.data || !isFormSession(component.data))
|
|
699
|
-
return null;
|
|
700
|
-
const session = component.data;
|
|
701
|
-
if (session.status === "active" || session.status === "ready") {
|
|
702
|
-
return session;
|
|
703
|
-
}
|
|
704
|
-
return null;
|
|
705
|
-
}
|
|
706
|
-
async function getAllActiveSessions(runtime, entityId) {
|
|
707
|
-
const components = await runtime.getComponents(entityId);
|
|
708
|
-
const sessions = [];
|
|
709
|
-
for (const component of components) {
|
|
710
|
-
if (component.type.startsWith(`${FORM_SESSION_COMPONENT}:`)) {
|
|
711
|
-
if (component.data && isFormSession(component.data)) {
|
|
712
|
-
const session = component.data;
|
|
713
|
-
if (session.status === "active" || session.status === "ready") {
|
|
714
|
-
sessions.push(session);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
return sessions;
|
|
720
|
-
}
|
|
721
|
-
async function getStashedSessions(runtime, entityId) {
|
|
722
|
-
const components = await runtime.getComponents(entityId);
|
|
723
|
-
const sessions = [];
|
|
724
|
-
for (const component of components) {
|
|
725
|
-
if (component.type.startsWith(`${FORM_SESSION_COMPONENT}:`)) {
|
|
726
|
-
if (component.data && isFormSession(component.data)) {
|
|
727
|
-
const session = component.data;
|
|
728
|
-
if (session.status === "stashed") {
|
|
729
|
-
sessions.push(session);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return sessions;
|
|
735
|
-
}
|
|
736
|
-
async function getSessionById(runtime, entityId, sessionId) {
|
|
737
|
-
const components = await runtime.getComponents(entityId);
|
|
738
|
-
for (const component of components) {
|
|
739
|
-
if (component.type.startsWith(`${FORM_SESSION_COMPONENT}:`)) {
|
|
740
|
-
if (component.data && isFormSession(component.data)) {
|
|
741
|
-
const session = component.data;
|
|
742
|
-
if (session.id === sessionId) {
|
|
743
|
-
return session;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
return null;
|
|
749
|
-
}
|
|
750
|
-
async function saveSession(runtime, session) {
|
|
751
|
-
const componentType = `${FORM_SESSION_COMPONENT}:${session.roomId}`;
|
|
752
|
-
const existing = await runtime.getComponent(session.entityId, componentType);
|
|
753
|
-
const context = await resolveComponentContext(runtime, session.roomId);
|
|
754
|
-
const resolvedWorldId = existing?.worldId ?? context.worldId;
|
|
755
|
-
const component = {
|
|
756
|
-
id: existing?.id || v4_default(),
|
|
757
|
-
entityId: session.entityId,
|
|
758
|
-
agentId: runtime.agentId,
|
|
759
|
-
roomId: session.roomId,
|
|
760
|
-
worldId: resolvedWorldId,
|
|
761
|
-
sourceEntityId: runtime.agentId,
|
|
762
|
-
type: componentType,
|
|
763
|
-
createdAt: existing?.createdAt || Date.now(),
|
|
764
|
-
data: JSON.parse(JSON.stringify(session))
|
|
765
|
-
};
|
|
766
|
-
if (existing) {
|
|
767
|
-
await runtime.updateComponent(component);
|
|
768
|
-
} else {
|
|
769
|
-
await runtime.createComponent(component);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
async function deleteSession(runtime, session) {
|
|
773
|
-
const componentType = `${FORM_SESSION_COMPONENT}:${session.roomId}`;
|
|
774
|
-
const existing = await runtime.getComponent(session.entityId, componentType);
|
|
775
|
-
if (existing) {
|
|
776
|
-
await runtime.deleteComponent(existing.id);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
async function saveSubmission(runtime, submission) {
|
|
780
|
-
const componentType = `${FORM_SUBMISSION_COMPONENT}:${submission.formId}:${submission.id}`;
|
|
781
|
-
const context = await resolveComponentContext(runtime);
|
|
782
|
-
const component = {
|
|
783
|
-
id: v4_default(),
|
|
784
|
-
entityId: submission.entityId,
|
|
785
|
-
agentId: runtime.agentId,
|
|
786
|
-
roomId: context.roomId,
|
|
787
|
-
worldId: context.worldId,
|
|
788
|
-
sourceEntityId: runtime.agentId,
|
|
789
|
-
type: componentType,
|
|
790
|
-
createdAt: submission.submittedAt,
|
|
791
|
-
data: JSON.parse(JSON.stringify(submission))
|
|
792
|
-
};
|
|
793
|
-
await runtime.createComponent(component);
|
|
794
|
-
}
|
|
795
|
-
async function getSubmissions(runtime, entityId, formId) {
|
|
796
|
-
const components = await runtime.getComponents(entityId);
|
|
797
|
-
const submissions = [];
|
|
798
|
-
const prefix = formId ? `${FORM_SUBMISSION_COMPONENT}:${formId}:` : `${FORM_SUBMISSION_COMPONENT}:`;
|
|
799
|
-
for (const component of components) {
|
|
800
|
-
if (component.type.startsWith(prefix)) {
|
|
801
|
-
if (component.data && isFormSubmission(component.data)) {
|
|
802
|
-
submissions.push(component.data);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
submissions.sort((a, b) => b.submittedAt - a.submittedAt);
|
|
807
|
-
return submissions;
|
|
808
|
-
}
|
|
809
|
-
async function getAutofillData(runtime, entityId, formId) {
|
|
810
|
-
const componentType = `${FORM_AUTOFILL_COMPONENT}:${formId}`;
|
|
811
|
-
const component = await runtime.getComponent(entityId, componentType);
|
|
812
|
-
if (!component?.data || !isFormAutofillData(component.data))
|
|
813
|
-
return null;
|
|
814
|
-
return component.data;
|
|
815
|
-
}
|
|
816
|
-
async function saveAutofillData(runtime, entityId, formId, values) {
|
|
817
|
-
const componentType = `${FORM_AUTOFILL_COMPONENT}:${formId}`;
|
|
818
|
-
const existing = await runtime.getComponent(entityId, componentType);
|
|
819
|
-
const context = await resolveComponentContext(runtime);
|
|
820
|
-
const resolvedWorldId = existing?.worldId ?? context.worldId;
|
|
821
|
-
const data = {
|
|
822
|
-
formId,
|
|
823
|
-
values,
|
|
824
|
-
updatedAt: Date.now()
|
|
825
|
-
};
|
|
826
|
-
const component = {
|
|
827
|
-
id: existing?.id || v4_default(),
|
|
828
|
-
entityId,
|
|
829
|
-
agentId: runtime.agentId,
|
|
830
|
-
roomId: context.roomId,
|
|
831
|
-
worldId: resolvedWorldId,
|
|
832
|
-
sourceEntityId: runtime.agentId,
|
|
833
|
-
type: componentType,
|
|
834
|
-
createdAt: existing?.createdAt || Date.now(),
|
|
835
|
-
data: JSON.parse(JSON.stringify(data))
|
|
836
|
-
};
|
|
837
|
-
if (existing) {
|
|
838
|
-
await runtime.updateComponent(component);
|
|
839
|
-
} else {
|
|
840
|
-
await runtime.createComponent(component);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value), resolveComponentContext = async (runtime, roomId) => {
|
|
844
|
-
if (roomId) {
|
|
845
|
-
const room = await runtime.getRoom(roomId);
|
|
846
|
-
return { roomId, worldId: room?.worldId ?? runtime.agentId };
|
|
847
|
-
}
|
|
848
|
-
return { roomId: runtime.agentId, worldId: runtime.agentId };
|
|
849
|
-
}, isFormSession = (data) => {
|
|
850
|
-
if (!isRecord(data))
|
|
851
|
-
return false;
|
|
852
|
-
return typeof data.id === "string" && typeof data.formId === "string" && typeof data.entityId === "string" && typeof data.roomId === "string";
|
|
853
|
-
}, isFormSubmission = (data) => {
|
|
854
|
-
if (!isRecord(data))
|
|
855
|
-
return false;
|
|
856
|
-
return typeof data.id === "string" && typeof data.formId === "string" && typeof data.sessionId === "string" && typeof data.entityId === "string";
|
|
857
|
-
}, isFormAutofillData = (data) => {
|
|
858
|
-
if (!isRecord(data))
|
|
859
|
-
return false;
|
|
860
|
-
return typeof data.formId === "string" && typeof data.updatedAt === "number" && typeof data.values === "object";
|
|
861
|
-
};
|
|
862
|
-
var init_storage = __esm(() => {
|
|
863
|
-
init_dist_node();
|
|
864
|
-
init_types();
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
// src/template.ts
|
|
868
|
-
function buildTemplateValues(session) {
|
|
869
|
-
const values = {};
|
|
870
|
-
for (const [key, state] of Object.entries(session.fields)) {
|
|
871
|
-
const value = state.value;
|
|
872
|
-
if (typeof value === "string") {
|
|
873
|
-
values[key] = value;
|
|
874
|
-
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
875
|
-
values[key] = String(value);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
const context = session.context;
|
|
879
|
-
if (context && typeof context === "object" && !Array.isArray(context)) {
|
|
880
|
-
for (const [key, value] of Object.entries(context)) {
|
|
881
|
-
if (typeof value === "string") {
|
|
882
|
-
values[key] = value;
|
|
883
|
-
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
884
|
-
values[key] = String(value);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
return values;
|
|
889
|
-
}
|
|
890
|
-
function renderTemplate(template, values) {
|
|
891
|
-
if (!template) {
|
|
892
|
-
return template;
|
|
893
|
-
}
|
|
894
|
-
return template.replace(TEMPLATE_PATTERN, (match, key) => {
|
|
895
|
-
const replacement = values[key];
|
|
896
|
-
return replacement !== undefined ? replacement : match;
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
function resolveControlTemplates(control, values) {
|
|
900
|
-
const resolvedOptions = control.options?.map((option) => ({
|
|
901
|
-
...option,
|
|
902
|
-
label: renderTemplate(option.label, values) ?? option.label,
|
|
903
|
-
description: renderTemplate(option.description, values)
|
|
904
|
-
}));
|
|
905
|
-
const resolvedFields = control.fields?.map((field) => resolveControlTemplates(field, values));
|
|
906
|
-
return {
|
|
907
|
-
...control,
|
|
908
|
-
label: renderTemplate(control.label, values) ?? control.label,
|
|
909
|
-
description: renderTemplate(control.description, values),
|
|
910
|
-
askPrompt: renderTemplate(control.askPrompt, values),
|
|
911
|
-
example: renderTemplate(control.example, values),
|
|
912
|
-
extractHints: control.extractHints?.map((hint) => renderTemplate(hint, values) ?? hint),
|
|
913
|
-
options: resolvedOptions,
|
|
914
|
-
fields: resolvedFields ?? control.fields
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
var TEMPLATE_PATTERN;
|
|
918
|
-
var init_template = __esm(() => {
|
|
919
|
-
TEMPLATE_PATTERN = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
// src/extraction.ts
|
|
923
|
-
import { ModelType, parseKeyValueXml } from "@elizaos/core";
|
|
924
|
-
async function llmIntentAndExtract(runtime, text, form, controls, templateValues) {
|
|
925
|
-
const resolvedControls = templateValues ? controls.map((control) => resolveControlTemplates(control, templateValues)) : controls;
|
|
926
|
-
const fieldsDescription = resolvedControls.filter((c) => !c.hidden).map((c) => {
|
|
927
|
-
const handler = getTypeHandler(c.type);
|
|
928
|
-
const typeHint = handler?.extractionPrompt || c.type;
|
|
929
|
-
const hints = c.extractHints?.join(", ") || "";
|
|
930
|
-
const options = c.options?.map((o) => o.value).join(", ") || "";
|
|
931
|
-
return `- ${c.key} (${c.label}): ${c.description || typeHint}${hints ? ` [hints: ${hints}]` : ""}${options ? ` [options: ${options}]` : ""}`;
|
|
932
|
-
}).join(`
|
|
933
|
-
`);
|
|
934
|
-
const prompt = `You are extracting structured data from a user's natural language message.
|
|
935
|
-
|
|
936
|
-
FORM: ${form.name}
|
|
937
|
-
${form.description ? `DESCRIPTION: ${form.description}` : ""}
|
|
938
|
-
|
|
939
|
-
FIELDS TO EXTRACT:
|
|
940
|
-
${fieldsDescription}
|
|
941
|
-
|
|
942
|
-
USER MESSAGE:
|
|
943
|
-
"${text}"
|
|
944
|
-
|
|
945
|
-
INSTRUCTIONS:
|
|
946
|
-
1. Determine the user's intent:
|
|
947
|
-
- fill_form: They are providing information for form fields
|
|
948
|
-
- submit: They want to submit/complete the form ("done", "submit", "finish", "that's all")
|
|
949
|
-
- stash: They want to save for later ("save for later", "pause", "hold on")
|
|
950
|
-
- restore: They want to resume a saved form ("resume", "continue", "pick up where")
|
|
951
|
-
- cancel: They want to cancel ("cancel", "abort", "nevermind", "forget it")
|
|
952
|
-
- undo: They want to undo last change ("undo", "go back", "wait no")
|
|
953
|
-
- skip: They want to skip current field ("skip", "pass", "don't know")
|
|
954
|
-
- explain: They want explanation ("why?", "what's that for?")
|
|
955
|
-
- example: They want an example ("example?", "like what?")
|
|
956
|
-
- progress: They want progress update ("how far?", "status")
|
|
957
|
-
- autofill: They want to use saved values ("same as last time")
|
|
958
|
-
- other: None of the above
|
|
959
|
-
|
|
960
|
-
2. For fill_form intent, extract all field values mentioned.
|
|
961
|
-
- For each extracted value, provide a confidence score (0.0-1.0)
|
|
962
|
-
- Note if this appears to be a correction to a previous value
|
|
963
|
-
|
|
964
|
-
Respond in this exact XML format:
|
|
965
|
-
<response>
|
|
966
|
-
<intent>fill_form|submit|stash|restore|cancel|undo|skip|explain|example|progress|autofill|other</intent>
|
|
967
|
-
<extractions>
|
|
968
|
-
<field>
|
|
969
|
-
<key>field_key</key>
|
|
970
|
-
<value>extracted_value</value>
|
|
971
|
-
<confidence>0.0-1.0</confidence>
|
|
972
|
-
<reasoning>why this value was extracted</reasoning>
|
|
973
|
-
<is_correction>true|false</is_correction>
|
|
974
|
-
</field>
|
|
975
|
-
<!-- more fields if applicable -->
|
|
976
|
-
</extractions>
|
|
977
|
-
</response>`;
|
|
978
|
-
try {
|
|
979
|
-
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
980
|
-
prompt,
|
|
981
|
-
temperature: 0.1
|
|
982
|
-
});
|
|
983
|
-
const parsed = parseExtractionResponse(response);
|
|
984
|
-
for (const extraction of parsed.extractions) {
|
|
985
|
-
const control = resolvedControls.find((c) => c.key === extraction.field);
|
|
986
|
-
if (control) {
|
|
987
|
-
if (typeof extraction.value === "string") {
|
|
988
|
-
extraction.value = parseValue(extraction.value, control);
|
|
989
|
-
}
|
|
990
|
-
const validation = validateField(extraction.value, control);
|
|
991
|
-
if (!validation.valid) {
|
|
992
|
-
extraction.confidence = Math.min(extraction.confidence, 0.3);
|
|
993
|
-
extraction.reasoning = `${extraction.reasoning || ""} (Validation failed: ${validation.error})`;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
if (form.debug) {
|
|
998
|
-
runtime.logger.debug("[FormExtraction] LLM extraction result:", JSON.stringify(parsed));
|
|
999
|
-
}
|
|
1000
|
-
return parsed;
|
|
1001
|
-
} catch (error) {
|
|
1002
|
-
runtime.logger.error("[FormExtraction] LLM extraction failed:", String(error));
|
|
1003
|
-
return { intent: "other", extractions: [] };
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
function parseExtractionResponse(response) {
|
|
1007
|
-
const result = {
|
|
1008
|
-
intent: "other",
|
|
1009
|
-
extractions: []
|
|
1010
|
-
};
|
|
1011
|
-
try {
|
|
1012
|
-
const parsed = parseKeyValueXml(response);
|
|
1013
|
-
if (parsed) {
|
|
1014
|
-
const intentStr = parsed.intent?.toLowerCase() ?? "other";
|
|
1015
|
-
result.intent = isValidIntent(intentStr) ? intentStr : "other";
|
|
1016
|
-
if (parsed.extractions) {
|
|
1017
|
-
const fields = Array.isArray(parsed.extractions) ? parsed.extractions : parsed.extractions.field ? Array.isArray(parsed.extractions.field) ? parsed.extractions.field : [parsed.extractions.field] : [];
|
|
1018
|
-
for (const field of fields) {
|
|
1019
|
-
if (field?.key) {
|
|
1020
|
-
const extraction = {
|
|
1021
|
-
field: String(field.key),
|
|
1022
|
-
value: field.value ?? null,
|
|
1023
|
-
confidence: parseFloat(String(field.confidence ?? "")) || 0.5,
|
|
1024
|
-
reasoning: field.reasoning ? String(field.reasoning) : undefined,
|
|
1025
|
-
isCorrection: field.is_correction === "true" || field.is_correction === true
|
|
1026
|
-
};
|
|
1027
|
-
result.extractions.push(extraction);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
} catch (_error) {
|
|
1033
|
-
const intentMatch = response.match(/<intent>([^<]+)<\/intent>/);
|
|
1034
|
-
if (intentMatch) {
|
|
1035
|
-
const intentStr = intentMatch[1].toLowerCase().trim();
|
|
1036
|
-
result.intent = isValidIntent(intentStr) ? intentStr : "other";
|
|
1037
|
-
}
|
|
1038
|
-
const fieldMatches = response.matchAll(/<field>\s*<key>([^<]+)<\/key>\s*<value>([^<]*)<\/value>\s*<confidence>([^<]+)<\/confidence>/g);
|
|
1039
|
-
for (const match of fieldMatches) {
|
|
1040
|
-
result.extractions.push({
|
|
1041
|
-
field: match[1].trim(),
|
|
1042
|
-
value: match[2].trim(),
|
|
1043
|
-
confidence: parseFloat(match[3]) || 0.5
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
return result;
|
|
1048
|
-
}
|
|
1049
|
-
function isValidIntent(str) {
|
|
1050
|
-
const validIntents = [
|
|
1051
|
-
"fill_form",
|
|
1052
|
-
"submit",
|
|
1053
|
-
"stash",
|
|
1054
|
-
"restore",
|
|
1055
|
-
"cancel",
|
|
1056
|
-
"undo",
|
|
1057
|
-
"skip",
|
|
1058
|
-
"explain",
|
|
1059
|
-
"example",
|
|
1060
|
-
"progress",
|
|
1061
|
-
"autofill",
|
|
1062
|
-
"other"
|
|
1063
|
-
];
|
|
1064
|
-
return validIntents.includes(str);
|
|
1065
|
-
}
|
|
1066
|
-
async function extractSingleField(runtime, text, control, debug, templateValues) {
|
|
1067
|
-
const resolvedControl = templateValues ? resolveControlTemplates(control, templateValues) : control;
|
|
1068
|
-
const handler = getTypeHandler(resolvedControl.type);
|
|
1069
|
-
const typeHint = handler?.extractionPrompt || resolvedControl.type;
|
|
1070
|
-
const prompt = `Extract the ${resolvedControl.label} (${typeHint}) from this message:
|
|
1071
|
-
|
|
1072
|
-
"${text}"
|
|
1073
|
-
|
|
1074
|
-
${resolvedControl.description ? `Context: ${resolvedControl.description}` : ""}
|
|
1075
|
-
${resolvedControl.extractHints?.length ? `Look for: ${resolvedControl.extractHints.join(", ")}` : ""}
|
|
1076
|
-
${resolvedControl.options?.length ? `Valid options: ${resolvedControl.options.map((o) => o.value).join(", ")}` : ""}
|
|
1077
|
-
${resolvedControl.example ? `Example: ${resolvedControl.example}` : ""}
|
|
1078
|
-
|
|
1079
|
-
Respond in XML:
|
|
1080
|
-
<response>
|
|
1081
|
-
<found>true|false</found>
|
|
1082
|
-
<value>extracted_value or empty if not found</value>
|
|
1083
|
-
<confidence>0.0-1.0</confidence>
|
|
1084
|
-
<reasoning>brief explanation</reasoning>
|
|
1085
|
-
</response>`;
|
|
1086
|
-
try {
|
|
1087
|
-
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
1088
|
-
prompt,
|
|
1089
|
-
temperature: 0.1
|
|
1090
|
-
});
|
|
1091
|
-
const parsed = parseKeyValueXml(response);
|
|
1092
|
-
const found = parsed?.found === true || parsed?.found === "true";
|
|
1093
|
-
if (found) {
|
|
1094
|
-
let value = parsed.value;
|
|
1095
|
-
if (typeof value === "string") {
|
|
1096
|
-
value = parseValue(value, resolvedControl);
|
|
1097
|
-
}
|
|
1098
|
-
const confidence = typeof parsed?.confidence === "number" ? parsed.confidence : parseFloat(String(parsed?.confidence ?? ""));
|
|
1099
|
-
const result = {
|
|
1100
|
-
field: resolvedControl.key,
|
|
1101
|
-
value: value ?? null,
|
|
1102
|
-
confidence: Number.isFinite(confidence) ? confidence : 0.5,
|
|
1103
|
-
reasoning: parsed.reasoning ? String(parsed.reasoning) : undefined
|
|
1104
|
-
};
|
|
1105
|
-
if (debug) {
|
|
1106
|
-
runtime.logger.debug("[FormExtraction] Single field extraction:", JSON.stringify(result));
|
|
1107
|
-
}
|
|
1108
|
-
return result;
|
|
1109
|
-
}
|
|
1110
|
-
return null;
|
|
1111
|
-
} catch (error) {
|
|
1112
|
-
runtime.logger.error("[FormExtraction] Single field extraction failed:", String(error));
|
|
1113
|
-
return null;
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
async function detectCorrection(runtime, text, currentValues, controls, templateValues) {
|
|
1117
|
-
const resolvedControls = templateValues ? controls.map((control) => resolveControlTemplates(control, templateValues)) : controls;
|
|
1118
|
-
const currentValuesStr = resolvedControls.filter((c) => currentValues[c.key] !== undefined).map((c) => `- ${c.label}: ${currentValues[c.key]}`).join(`
|
|
1119
|
-
`);
|
|
1120
|
-
if (!currentValuesStr) {
|
|
1121
|
-
return [];
|
|
1122
|
-
}
|
|
1123
|
-
const prompt = `Is the user correcting any of these previously provided values?
|
|
1124
|
-
|
|
1125
|
-
Current values:
|
|
1126
|
-
${currentValuesStr}
|
|
1127
|
-
|
|
1128
|
-
User message:
|
|
1129
|
-
"${text}"
|
|
1130
|
-
|
|
1131
|
-
If they are correcting a value, extract the new value. Otherwise respond with no corrections.
|
|
1132
|
-
|
|
1133
|
-
Respond in XML:
|
|
1134
|
-
<response>
|
|
1135
|
-
<has_correction>true|false</has_correction>
|
|
1136
|
-
<corrections>
|
|
1137
|
-
<correction>
|
|
1138
|
-
<field>field_label</field>
|
|
1139
|
-
<old_value>previous value</old_value>
|
|
1140
|
-
<new_value>corrected value</new_value>
|
|
1141
|
-
<confidence>0.0-1.0</confidence>
|
|
1142
|
-
</correction>
|
|
1143
|
-
</corrections>
|
|
1144
|
-
</response>`;
|
|
1145
|
-
try {
|
|
1146
|
-
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
1147
|
-
prompt,
|
|
1148
|
-
temperature: 0.1
|
|
1149
|
-
});
|
|
1150
|
-
const parsed = parseKeyValueXml(response);
|
|
1151
|
-
const hasCorrection = parsed?.has_correction === true || parsed?.has_correction === "true";
|
|
1152
|
-
if (parsed && hasCorrection && parsed.corrections) {
|
|
1153
|
-
const corrections = [];
|
|
1154
|
-
const correctionList = Array.isArray(parsed.corrections) ? parsed.corrections : parsed.corrections.correction ? Array.isArray(parsed.corrections.correction) ? parsed.corrections.correction : [parsed.corrections.correction] : [];
|
|
1155
|
-
for (const correction of correctionList) {
|
|
1156
|
-
const fieldName = correction.field ? String(correction.field) : "";
|
|
1157
|
-
const control = resolvedControls.find((c) => c.label.toLowerCase() === fieldName.toLowerCase() || c.key.toLowerCase() === fieldName.toLowerCase());
|
|
1158
|
-
if (control) {
|
|
1159
|
-
let value = correction.new_value;
|
|
1160
|
-
if (typeof value === "string") {
|
|
1161
|
-
value = parseValue(value, control);
|
|
1162
|
-
}
|
|
1163
|
-
const confidence = typeof correction.confidence === "number" ? correction.confidence : parseFloat(String(correction.confidence ?? ""));
|
|
1164
|
-
const extraction = {
|
|
1165
|
-
field: control.key,
|
|
1166
|
-
value: value ?? null,
|
|
1167
|
-
confidence: Number.isFinite(confidence) ? confidence : 0.8,
|
|
1168
|
-
isCorrection: true
|
|
1169
|
-
};
|
|
1170
|
-
corrections.push(extraction);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
return corrections;
|
|
1174
|
-
}
|
|
1175
|
-
return [];
|
|
1176
|
-
} catch (error) {
|
|
1177
|
-
runtime.logger.error("[FormExtraction] Correction detection failed:", String(error));
|
|
1178
|
-
return [];
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
var init_extraction = __esm(() => {
|
|
1182
|
-
init_template();
|
|
1183
|
-
init_validation();
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
// src/service.ts
|
|
1187
|
-
var exports_service = {};
|
|
1188
|
-
__export(exports_service, {
|
|
1189
|
-
FormService: () => FormService
|
|
1190
|
-
});
|
|
1191
|
-
import {
|
|
1192
|
-
logger,
|
|
1193
|
-
Service
|
|
1194
|
-
} from "@elizaos/core";
|
|
1195
|
-
function prettify3(key) {
|
|
1196
|
-
return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1197
|
-
}
|
|
1198
|
-
var FormService;
|
|
1199
|
-
var init_service = __esm(() => {
|
|
1200
|
-
init_dist_node();
|
|
1201
|
-
init_builtins();
|
|
1202
|
-
init_storage();
|
|
1203
|
-
init_types();
|
|
1204
|
-
init_validation();
|
|
1205
|
-
FormService = class FormService extends Service {
|
|
1206
|
-
static serviceType = "FORM";
|
|
1207
|
-
capabilityDescription = "Manages conversational forms for data collection";
|
|
1208
|
-
forms = new Map;
|
|
1209
|
-
controlTypes = new Map;
|
|
1210
|
-
static async start(runtime) {
|
|
1211
|
-
const service = new FormService(runtime);
|
|
1212
|
-
registerBuiltinTypes((type, options) => service.registerControlType(type, options));
|
|
1213
|
-
logger.info("[FormService] Started with built-in types");
|
|
1214
|
-
return service;
|
|
1215
|
-
}
|
|
1216
|
-
async stop() {
|
|
1217
|
-
logger.info("[FormService] Stopped");
|
|
1218
|
-
}
|
|
1219
|
-
registerForm(definition) {
|
|
1220
|
-
const form = {
|
|
1221
|
-
...definition,
|
|
1222
|
-
version: definition.version ?? FORM_DEFINITION_DEFAULTS.version,
|
|
1223
|
-
status: definition.status ?? FORM_DEFINITION_DEFAULTS.status,
|
|
1224
|
-
ux: { ...FORM_DEFINITION_DEFAULTS.ux, ...definition.ux },
|
|
1225
|
-
ttl: { ...FORM_DEFINITION_DEFAULTS.ttl, ...definition.ttl },
|
|
1226
|
-
nudge: { ...FORM_DEFINITION_DEFAULTS.nudge, ...definition.nudge },
|
|
1227
|
-
debug: definition.debug ?? FORM_DEFINITION_DEFAULTS.debug,
|
|
1228
|
-
controls: definition.controls.map((control) => ({
|
|
1229
|
-
...control,
|
|
1230
|
-
type: control.type || FORM_CONTROL_DEFAULTS.type,
|
|
1231
|
-
required: control.required ?? FORM_CONTROL_DEFAULTS.required,
|
|
1232
|
-
confirmThreshold: control.confirmThreshold ?? FORM_CONTROL_DEFAULTS.confirmThreshold,
|
|
1233
|
-
label: control.label || prettify3(control.key)
|
|
1234
|
-
}))
|
|
1235
|
-
};
|
|
1236
|
-
this.forms.set(form.id, form);
|
|
1237
|
-
logger.debug(`[FormService] Registered form: ${form.id}`);
|
|
1238
|
-
}
|
|
1239
|
-
getForm(formId) {
|
|
1240
|
-
return this.forms.get(formId);
|
|
1241
|
-
}
|
|
1242
|
-
listForms() {
|
|
1243
|
-
return Array.from(this.forms.values());
|
|
1244
|
-
}
|
|
1245
|
-
registerControlType(type, options) {
|
|
1246
|
-
const existing = this.controlTypes.get(type.id);
|
|
1247
|
-
if (existing) {
|
|
1248
|
-
if (existing.builtin && !options?.allowOverride) {
|
|
1249
|
-
logger.warn(`[FormService] Cannot override builtin type '${type.id}' without allowOverride: true`);
|
|
1250
|
-
return;
|
|
1251
|
-
}
|
|
1252
|
-
logger.warn(`[FormService] Overriding control type: ${type.id}`);
|
|
1253
|
-
}
|
|
1254
|
-
this.controlTypes.set(type.id, type);
|
|
1255
|
-
logger.debug(`[FormService] Registered control type: ${type.id}`);
|
|
1256
|
-
}
|
|
1257
|
-
getControlType(typeId) {
|
|
1258
|
-
return this.controlTypes.get(typeId);
|
|
1259
|
-
}
|
|
1260
|
-
listControlTypes() {
|
|
1261
|
-
return Array.from(this.controlTypes.values());
|
|
1262
|
-
}
|
|
1263
|
-
isCompositeType(typeId) {
|
|
1264
|
-
const type = this.controlTypes.get(typeId);
|
|
1265
|
-
return !!type?.getSubControls;
|
|
1266
|
-
}
|
|
1267
|
-
isExternalType(typeId) {
|
|
1268
|
-
const type = this.controlTypes.get(typeId);
|
|
1269
|
-
return !!type?.activate;
|
|
1270
|
-
}
|
|
1271
|
-
getSubControls(control) {
|
|
1272
|
-
const type = this.controlTypes.get(control.type);
|
|
1273
|
-
if (!type?.getSubControls) {
|
|
1274
|
-
return [];
|
|
1275
|
-
}
|
|
1276
|
-
return type.getSubControls(control, this.runtime);
|
|
1277
|
-
}
|
|
1278
|
-
async startSession(formId, entityId, roomId, options) {
|
|
1279
|
-
const form = this.getForm(formId);
|
|
1280
|
-
if (!form) {
|
|
1281
|
-
throw new Error(`Form not found: ${formId}`);
|
|
1282
|
-
}
|
|
1283
|
-
const existing = await getActiveSession(this.runtime, entityId, roomId);
|
|
1284
|
-
if (existing) {
|
|
1285
|
-
throw new Error(`Active session already exists for this user/room: ${existing.id}`);
|
|
1286
|
-
}
|
|
1287
|
-
const now = Date.now();
|
|
1288
|
-
const fields = {};
|
|
1289
|
-
for (const control of form.controls) {
|
|
1290
|
-
if (options?.initialValues?.[control.key] !== undefined) {
|
|
1291
|
-
fields[control.key] = {
|
|
1292
|
-
status: "filled",
|
|
1293
|
-
value: options.initialValues[control.key],
|
|
1294
|
-
source: "manual",
|
|
1295
|
-
updatedAt: now
|
|
1296
|
-
};
|
|
1297
|
-
} else if (control.defaultValue !== undefined) {
|
|
1298
|
-
fields[control.key] = {
|
|
1299
|
-
status: "filled",
|
|
1300
|
-
value: control.defaultValue,
|
|
1301
|
-
source: "default",
|
|
1302
|
-
updatedAt: now
|
|
1303
|
-
};
|
|
1304
|
-
} else {
|
|
1305
|
-
fields[control.key] = { status: "empty" };
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
const ttlDays = form.ttl?.minDays ?? 14;
|
|
1309
|
-
const expiresAt = now + ttlDays * 24 * 60 * 60 * 1000;
|
|
1310
|
-
const session = {
|
|
1311
|
-
id: v4_default(),
|
|
1312
|
-
formId,
|
|
1313
|
-
formVersion: form.version,
|
|
1314
|
-
entityId,
|
|
1315
|
-
roomId,
|
|
1316
|
-
status: "active",
|
|
1317
|
-
fields,
|
|
1318
|
-
history: [],
|
|
1319
|
-
context: options?.context,
|
|
1320
|
-
locale: options?.locale,
|
|
1321
|
-
effort: {
|
|
1322
|
-
interactionCount: 0,
|
|
1323
|
-
timeSpentMs: 0,
|
|
1324
|
-
firstInteractionAt: now,
|
|
1325
|
-
lastInteractionAt: now
|
|
1326
|
-
},
|
|
1327
|
-
expiresAt,
|
|
1328
|
-
createdAt: now,
|
|
1329
|
-
updatedAt: now
|
|
1330
|
-
};
|
|
1331
|
-
await saveSession(this.runtime, session);
|
|
1332
|
-
if (form.hooks?.onStart) {
|
|
1333
|
-
await this.executeHook(session, "onStart");
|
|
1334
|
-
}
|
|
1335
|
-
logger.debug(`[FormService] Started session ${session.id} for form ${formId}`);
|
|
1336
|
-
return session;
|
|
1337
|
-
}
|
|
1338
|
-
async getActiveSession(entityId, roomId) {
|
|
1339
|
-
return getActiveSession(this.runtime, entityId, roomId);
|
|
1340
|
-
}
|
|
1341
|
-
async getAllActiveSessions(entityId) {
|
|
1342
|
-
return getAllActiveSessions(this.runtime, entityId);
|
|
1343
|
-
}
|
|
1344
|
-
async getStashedSessions(entityId) {
|
|
1345
|
-
return getStashedSessions(this.runtime, entityId);
|
|
1346
|
-
}
|
|
1347
|
-
async saveSession(session) {
|
|
1348
|
-
session.updatedAt = Date.now();
|
|
1349
|
-
await saveSession(this.runtime, session);
|
|
1350
|
-
}
|
|
1351
|
-
async updateField(sessionId, entityId, field, value, confidence, source, messageId) {
|
|
1352
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1353
|
-
if (!session) {
|
|
1354
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1355
|
-
}
|
|
1356
|
-
const form = this.getForm(session.formId);
|
|
1357
|
-
if (!form) {
|
|
1358
|
-
throw new Error(`Form not found: ${session.formId}`);
|
|
1359
|
-
}
|
|
1360
|
-
const control = form.controls.find((c) => c.key === field);
|
|
1361
|
-
if (!control) {
|
|
1362
|
-
throw new Error(`Field not found: ${field}`);
|
|
1363
|
-
}
|
|
1364
|
-
const oldValue = session.fields[field]?.value;
|
|
1365
|
-
const validation = validateField(value, control);
|
|
1366
|
-
let status;
|
|
1367
|
-
if (!validation.valid) {
|
|
1368
|
-
status = "invalid";
|
|
1369
|
-
} else if (confidence < (control.confirmThreshold ?? 0.8)) {
|
|
1370
|
-
status = "uncertain";
|
|
1371
|
-
} else {
|
|
1372
|
-
status = "filled";
|
|
1373
|
-
}
|
|
1374
|
-
const now = Date.now();
|
|
1375
|
-
if (oldValue !== undefined) {
|
|
1376
|
-
const historyEntry = {
|
|
1377
|
-
field,
|
|
1378
|
-
oldValue,
|
|
1379
|
-
newValue: value,
|
|
1380
|
-
timestamp: now
|
|
1381
|
-
};
|
|
1382
|
-
session.history.push(historyEntry);
|
|
1383
|
-
const maxUndo = form.ux?.maxUndoSteps ?? 5;
|
|
1384
|
-
if (session.history.length > maxUndo) {
|
|
1385
|
-
session.history = session.history.slice(-maxUndo);
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
session.fields[field] = {
|
|
1389
|
-
status,
|
|
1390
|
-
value,
|
|
1391
|
-
confidence,
|
|
1392
|
-
source,
|
|
1393
|
-
messageId,
|
|
1394
|
-
updatedAt: now,
|
|
1395
|
-
error: !validation.valid ? validation.error : undefined
|
|
1396
|
-
};
|
|
1397
|
-
session.effort.interactionCount++;
|
|
1398
|
-
session.effort.lastInteractionAt = now;
|
|
1399
|
-
session.effort.timeSpentMs = now - session.effort.firstInteractionAt;
|
|
1400
|
-
session.expiresAt = this.calculateTTL(session);
|
|
1401
|
-
const allRequiredFilled = this.checkAllRequiredFilled(session, form);
|
|
1402
|
-
if (allRequiredFilled && session.status === "active") {
|
|
1403
|
-
session.status = "ready";
|
|
1404
|
-
if (form.hooks?.onReady) {
|
|
1405
|
-
await this.executeHook(session, "onReady");
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
session.updatedAt = now;
|
|
1409
|
-
await saveSession(this.runtime, session);
|
|
1410
|
-
if (form.hooks?.onFieldChange) {
|
|
1411
|
-
const hookPayload = { field, value };
|
|
1412
|
-
if (oldValue !== undefined) {
|
|
1413
|
-
hookPayload.oldValue = oldValue;
|
|
1414
|
-
}
|
|
1415
|
-
await this.executeHook(session, "onFieldChange", hookPayload);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
async undoLastChange(sessionId, entityId) {
|
|
1419
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1420
|
-
if (!session) {
|
|
1421
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1422
|
-
}
|
|
1423
|
-
const form = this.getForm(session.formId);
|
|
1424
|
-
if (!form?.ux?.allowUndo) {
|
|
1425
|
-
return null;
|
|
1426
|
-
}
|
|
1427
|
-
const lastChange = session.history.pop();
|
|
1428
|
-
if (!lastChange) {
|
|
1429
|
-
return null;
|
|
1430
|
-
}
|
|
1431
|
-
if (lastChange.oldValue !== undefined) {
|
|
1432
|
-
session.fields[lastChange.field] = {
|
|
1433
|
-
status: "filled",
|
|
1434
|
-
value: lastChange.oldValue,
|
|
1435
|
-
source: "correction",
|
|
1436
|
-
updatedAt: Date.now()
|
|
1437
|
-
};
|
|
1438
|
-
} else {
|
|
1439
|
-
session.fields[lastChange.field] = { status: "empty" };
|
|
1440
|
-
}
|
|
1441
|
-
session.updatedAt = Date.now();
|
|
1442
|
-
await saveSession(this.runtime, session);
|
|
1443
|
-
return { field: lastChange.field, restoredValue: lastChange.oldValue };
|
|
1444
|
-
}
|
|
1445
|
-
async skipField(sessionId, entityId, field) {
|
|
1446
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1447
|
-
if (!session) {
|
|
1448
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1449
|
-
}
|
|
1450
|
-
const form = this.getForm(session.formId);
|
|
1451
|
-
if (!form?.ux?.allowSkip) {
|
|
1452
|
-
return false;
|
|
1453
|
-
}
|
|
1454
|
-
const control = form.controls.find((c) => c.key === field);
|
|
1455
|
-
if (!control) {
|
|
1456
|
-
return false;
|
|
1457
|
-
}
|
|
1458
|
-
if (control.required) {
|
|
1459
|
-
return false;
|
|
1460
|
-
}
|
|
1461
|
-
session.fields[field] = {
|
|
1462
|
-
status: "skipped",
|
|
1463
|
-
updatedAt: Date.now()
|
|
1464
|
-
};
|
|
1465
|
-
session.updatedAt = Date.now();
|
|
1466
|
-
await saveSession(this.runtime, session);
|
|
1467
|
-
return true;
|
|
1468
|
-
}
|
|
1469
|
-
async confirmField(sessionId, entityId, field, accepted) {
|
|
1470
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1471
|
-
if (!session) {
|
|
1472
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1473
|
-
}
|
|
1474
|
-
const fieldState = session.fields[field];
|
|
1475
|
-
if (!fieldState || fieldState.status !== "uncertain") {
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
const now = Date.now();
|
|
1479
|
-
if (accepted) {
|
|
1480
|
-
fieldState.status = "filled";
|
|
1481
|
-
fieldState.confirmedAt = now;
|
|
1482
|
-
} else {
|
|
1483
|
-
fieldState.status = "empty";
|
|
1484
|
-
fieldState.value = undefined;
|
|
1485
|
-
fieldState.confidence = undefined;
|
|
1486
|
-
}
|
|
1487
|
-
fieldState.updatedAt = now;
|
|
1488
|
-
session.updatedAt = now;
|
|
1489
|
-
await saveSession(this.runtime, session);
|
|
1490
|
-
}
|
|
1491
|
-
async updateSubField(sessionId, entityId, parentField, subField, value, confidence, messageId) {
|
|
1492
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1493
|
-
if (!session) {
|
|
1494
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1495
|
-
}
|
|
1496
|
-
const form = this.getForm(session.formId);
|
|
1497
|
-
if (!form) {
|
|
1498
|
-
throw new Error(`Form not found: ${session.formId}`);
|
|
1499
|
-
}
|
|
1500
|
-
const parentControl = form.controls.find((c) => c.key === parentField);
|
|
1501
|
-
if (!parentControl) {
|
|
1502
|
-
throw new Error(`Parent field not found: ${parentField}`);
|
|
1503
|
-
}
|
|
1504
|
-
const controlType = this.getControlType(parentControl.type);
|
|
1505
|
-
if (!controlType?.getSubControls) {
|
|
1506
|
-
throw new Error(`Control type '${parentControl.type}' is not a composite type`);
|
|
1507
|
-
}
|
|
1508
|
-
const subControls = controlType.getSubControls(parentControl, this.runtime);
|
|
1509
|
-
const subControl = subControls.find((c) => c.key === subField);
|
|
1510
|
-
if (!subControl) {
|
|
1511
|
-
throw new Error(`Subfield not found: ${subField} in ${parentField}`);
|
|
1512
|
-
}
|
|
1513
|
-
const now = Date.now();
|
|
1514
|
-
if (!session.fields[parentField]) {
|
|
1515
|
-
session.fields[parentField] = { status: "empty" };
|
|
1516
|
-
}
|
|
1517
|
-
if (!session.fields[parentField].subFields) {
|
|
1518
|
-
session.fields[parentField].subFields = {};
|
|
1519
|
-
}
|
|
1520
|
-
let subFieldStatus;
|
|
1521
|
-
let error;
|
|
1522
|
-
if (controlType.validate) {
|
|
1523
|
-
const result = controlType.validate(value, subControl);
|
|
1524
|
-
if (!result.valid) {
|
|
1525
|
-
subFieldStatus = "invalid";
|
|
1526
|
-
error = result.error;
|
|
1527
|
-
} else if (confidence < (subControl.confirmThreshold ?? 0.8)) {
|
|
1528
|
-
subFieldStatus = "uncertain";
|
|
1529
|
-
} else {
|
|
1530
|
-
subFieldStatus = "filled";
|
|
1531
|
-
}
|
|
1532
|
-
} else {
|
|
1533
|
-
const validation = validateField(value, subControl);
|
|
1534
|
-
if (!validation.valid) {
|
|
1535
|
-
subFieldStatus = "invalid";
|
|
1536
|
-
error = validation.error;
|
|
1537
|
-
} else if (confidence < (subControl.confirmThreshold ?? 0.8)) {
|
|
1538
|
-
subFieldStatus = "uncertain";
|
|
1539
|
-
} else {
|
|
1540
|
-
subFieldStatus = "filled";
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
session.fields[parentField].subFields[subField] = {
|
|
1544
|
-
status: subFieldStatus,
|
|
1545
|
-
value,
|
|
1546
|
-
confidence,
|
|
1547
|
-
source: "extraction",
|
|
1548
|
-
messageId,
|
|
1549
|
-
updatedAt: now,
|
|
1550
|
-
error
|
|
1551
|
-
};
|
|
1552
|
-
session.effort.interactionCount++;
|
|
1553
|
-
session.effort.lastInteractionAt = now;
|
|
1554
|
-
session.effort.timeSpentMs = now - session.effort.firstInteractionAt;
|
|
1555
|
-
session.updatedAt = now;
|
|
1556
|
-
await saveSession(this.runtime, session);
|
|
1557
|
-
logger.debug(`[FormService] Updated subfield ${parentField}.${subField}`);
|
|
1558
|
-
}
|
|
1559
|
-
areSubFieldsFilled(session, parentField) {
|
|
1560
|
-
const form = this.getForm(session.formId);
|
|
1561
|
-
if (!form)
|
|
1562
|
-
return false;
|
|
1563
|
-
const parentControl = form.controls.find((c) => c.key === parentField);
|
|
1564
|
-
if (!parentControl)
|
|
1565
|
-
return false;
|
|
1566
|
-
const controlType = this.getControlType(parentControl.type);
|
|
1567
|
-
if (!controlType?.getSubControls)
|
|
1568
|
-
return false;
|
|
1569
|
-
const subControls = controlType.getSubControls(parentControl, this.runtime);
|
|
1570
|
-
const subFields = session.fields[parentField]?.subFields || {};
|
|
1571
|
-
for (const subControl of subControls) {
|
|
1572
|
-
if (!subControl.required)
|
|
1573
|
-
continue;
|
|
1574
|
-
const subField = subFields[subControl.key];
|
|
1575
|
-
if (!subField || subField.status !== "filled") {
|
|
1576
|
-
return false;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
return true;
|
|
1580
|
-
}
|
|
1581
|
-
getSubFieldValues(session, parentField) {
|
|
1582
|
-
const subFields = session.fields[parentField]?.subFields || {};
|
|
1583
|
-
const values = {};
|
|
1584
|
-
for (const [key, state] of Object.entries(subFields)) {
|
|
1585
|
-
if (state.value !== undefined) {
|
|
1586
|
-
values[key] = state.value;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
return values;
|
|
1590
|
-
}
|
|
1591
|
-
async activateExternalField(sessionId, entityId, field) {
|
|
1592
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1593
|
-
if (!session) {
|
|
1594
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1595
|
-
}
|
|
1596
|
-
const form = this.getForm(session.formId);
|
|
1597
|
-
if (!form) {
|
|
1598
|
-
throw new Error(`Form not found: ${session.formId}`);
|
|
1599
|
-
}
|
|
1600
|
-
const control = form.controls.find((c) => c.key === field);
|
|
1601
|
-
if (!control) {
|
|
1602
|
-
throw new Error(`Field not found: ${field}`);
|
|
1603
|
-
}
|
|
1604
|
-
const controlType = this.getControlType(control.type);
|
|
1605
|
-
if (!controlType?.activate) {
|
|
1606
|
-
throw new Error(`Control type '${control.type}' does not support activation`);
|
|
1607
|
-
}
|
|
1608
|
-
const subValues = this.getSubFieldValues(session, field);
|
|
1609
|
-
const context = {
|
|
1610
|
-
runtime: this.runtime,
|
|
1611
|
-
session,
|
|
1612
|
-
control,
|
|
1613
|
-
subValues
|
|
1614
|
-
};
|
|
1615
|
-
const activation = await controlType.activate(context);
|
|
1616
|
-
const now = Date.now();
|
|
1617
|
-
if (!session.fields[field]) {
|
|
1618
|
-
session.fields[field] = { status: "empty" };
|
|
1619
|
-
}
|
|
1620
|
-
session.fields[field].status = "pending";
|
|
1621
|
-
session.fields[field].externalState = {
|
|
1622
|
-
status: "pending",
|
|
1623
|
-
reference: activation.reference,
|
|
1624
|
-
instructions: activation.instructions,
|
|
1625
|
-
address: activation.address,
|
|
1626
|
-
activatedAt: now
|
|
1627
|
-
};
|
|
1628
|
-
session.updatedAt = now;
|
|
1629
|
-
await saveSession(this.runtime, session);
|
|
1630
|
-
logger.info(`[FormService] Activated external field ${field} with reference ${activation.reference}`);
|
|
1631
|
-
return activation;
|
|
1632
|
-
}
|
|
1633
|
-
async confirmExternalField(sessionId, entityId, field, value, externalData) {
|
|
1634
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1635
|
-
if (!session) {
|
|
1636
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1637
|
-
}
|
|
1638
|
-
const fieldState = session.fields[field];
|
|
1639
|
-
if (!fieldState || fieldState.status !== "pending") {
|
|
1640
|
-
logger.warn(`[FormService] Cannot confirm field ${field}: not in pending state`);
|
|
1641
|
-
return;
|
|
1642
|
-
}
|
|
1643
|
-
const now = Date.now();
|
|
1644
|
-
fieldState.status = "filled";
|
|
1645
|
-
fieldState.value = value;
|
|
1646
|
-
fieldState.source = "external";
|
|
1647
|
-
fieldState.updatedAt = now;
|
|
1648
|
-
if (fieldState.externalState) {
|
|
1649
|
-
fieldState.externalState.status = "confirmed";
|
|
1650
|
-
fieldState.externalState.confirmedAt = now;
|
|
1651
|
-
fieldState.externalState.externalData = externalData;
|
|
1652
|
-
}
|
|
1653
|
-
const form = this.getForm(session.formId);
|
|
1654
|
-
if (form && this.checkAllRequiredFilled(session, form)) {
|
|
1655
|
-
if (session.status === "active") {
|
|
1656
|
-
session.status = "ready";
|
|
1657
|
-
if (form.hooks?.onReady) {
|
|
1658
|
-
await this.executeHook(session, "onReady");
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
session.updatedAt = now;
|
|
1663
|
-
await saveSession(this.runtime, session);
|
|
1664
|
-
try {
|
|
1665
|
-
await this.runtime.emitEvent("FORM_FIELD_CONFIRMED", {
|
|
1666
|
-
runtime: this.runtime,
|
|
1667
|
-
sessionId,
|
|
1668
|
-
entityId,
|
|
1669
|
-
field,
|
|
1670
|
-
value,
|
|
1671
|
-
externalData
|
|
1672
|
-
});
|
|
1673
|
-
} catch (_error) {
|
|
1674
|
-
logger.debug(`[FormService] No event handler for FORM_FIELD_CONFIRMED`);
|
|
1675
|
-
}
|
|
1676
|
-
logger.info(`[FormService] Confirmed external field ${field}`);
|
|
1677
|
-
}
|
|
1678
|
-
async cancelExternalField(sessionId, entityId, field, reason) {
|
|
1679
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1680
|
-
if (!session) {
|
|
1681
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1682
|
-
}
|
|
1683
|
-
const form = this.getForm(session.formId);
|
|
1684
|
-
const control = form?.controls.find((c) => c.key === field);
|
|
1685
|
-
const controlType = control ? this.getControlType(control.type) : undefined;
|
|
1686
|
-
if (controlType?.deactivate && control) {
|
|
1687
|
-
try {
|
|
1688
|
-
await controlType.deactivate({
|
|
1689
|
-
runtime: this.runtime,
|
|
1690
|
-
session,
|
|
1691
|
-
control,
|
|
1692
|
-
subValues: this.getSubFieldValues(session, field)
|
|
1693
|
-
});
|
|
1694
|
-
} catch (error) {
|
|
1695
|
-
logger.error(`[FormService] Deactivate failed for ${field}: ${String(error)}`);
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
const fieldState = session.fields[field];
|
|
1699
|
-
if (fieldState) {
|
|
1700
|
-
fieldState.status = "empty";
|
|
1701
|
-
fieldState.error = reason;
|
|
1702
|
-
if (fieldState.externalState) {
|
|
1703
|
-
fieldState.externalState.status = "failed";
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
session.updatedAt = Date.now();
|
|
1707
|
-
await saveSession(this.runtime, session);
|
|
1708
|
-
try {
|
|
1709
|
-
await this.runtime.emitEvent("FORM_FIELD_CANCELLED", {
|
|
1710
|
-
runtime: this.runtime,
|
|
1711
|
-
sessionId,
|
|
1712
|
-
entityId,
|
|
1713
|
-
field,
|
|
1714
|
-
reason
|
|
1715
|
-
});
|
|
1716
|
-
} catch (_error) {
|
|
1717
|
-
logger.debug(`[FormService] No event handler for FORM_FIELD_CANCELLED`);
|
|
1718
|
-
}
|
|
1719
|
-
logger.info(`[FormService] Cancelled external field ${field}: ${reason}`);
|
|
1720
|
-
}
|
|
1721
|
-
async submit(sessionId, entityId) {
|
|
1722
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1723
|
-
if (!session) {
|
|
1724
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1725
|
-
}
|
|
1726
|
-
const form = this.getForm(session.formId);
|
|
1727
|
-
if (!form) {
|
|
1728
|
-
throw new Error(`Form not found: ${session.formId}`);
|
|
1729
|
-
}
|
|
1730
|
-
if (!this.checkAllRequiredFilled(session, form)) {
|
|
1731
|
-
throw new Error("Not all required fields are filled");
|
|
1732
|
-
}
|
|
1733
|
-
const now = Date.now();
|
|
1734
|
-
const values = {};
|
|
1735
|
-
const mappedValues = {};
|
|
1736
|
-
const files = {};
|
|
1737
|
-
for (const control of form.controls) {
|
|
1738
|
-
const fieldState = session.fields[control.key];
|
|
1739
|
-
if (fieldState?.value !== undefined) {
|
|
1740
|
-
values[control.key] = fieldState.value;
|
|
1741
|
-
const dbKey = control.dbbind || control.key;
|
|
1742
|
-
mappedValues[dbKey] = fieldState.value;
|
|
1743
|
-
}
|
|
1744
|
-
if (fieldState?.files) {
|
|
1745
|
-
files[control.key] = fieldState.files;
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
const submission = {
|
|
1749
|
-
id: v4_default(),
|
|
1750
|
-
formId: session.formId,
|
|
1751
|
-
formVersion: session.formVersion,
|
|
1752
|
-
sessionId: session.id,
|
|
1753
|
-
entityId: session.entityId,
|
|
1754
|
-
values,
|
|
1755
|
-
mappedValues,
|
|
1756
|
-
files: Object.keys(files).length > 0 ? files : undefined,
|
|
1757
|
-
submittedAt: now,
|
|
1758
|
-
meta: session.meta
|
|
1759
|
-
};
|
|
1760
|
-
await saveSubmission(this.runtime, submission);
|
|
1761
|
-
await saveAutofillData(this.runtime, entityId, session.formId, values);
|
|
1762
|
-
session.status = "submitted";
|
|
1763
|
-
session.submittedAt = now;
|
|
1764
|
-
session.updatedAt = now;
|
|
1765
|
-
await saveSession(this.runtime, session);
|
|
1766
|
-
if (form.hooks?.onSubmit) {
|
|
1767
|
-
const submissionPayload = JSON.parse(JSON.stringify(submission));
|
|
1768
|
-
await this.executeHook(session, "onSubmit", {
|
|
1769
|
-
submission: submissionPayload
|
|
1770
|
-
});
|
|
1771
|
-
}
|
|
1772
|
-
logger.debug(`[FormService] Submitted session ${sessionId}`);
|
|
1773
|
-
return submission;
|
|
1774
|
-
}
|
|
1775
|
-
async stash(sessionId, entityId) {
|
|
1776
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1777
|
-
if (!session) {
|
|
1778
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1779
|
-
}
|
|
1780
|
-
const form = this.getForm(session.formId);
|
|
1781
|
-
session.status = "stashed";
|
|
1782
|
-
session.updatedAt = Date.now();
|
|
1783
|
-
await saveSession(this.runtime, session);
|
|
1784
|
-
if (form?.hooks?.onCancel) {}
|
|
1785
|
-
logger.debug(`[FormService] Stashed session ${sessionId}`);
|
|
1786
|
-
}
|
|
1787
|
-
async restore(sessionId, entityId) {
|
|
1788
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1789
|
-
if (!session) {
|
|
1790
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1791
|
-
}
|
|
1792
|
-
if (session.status !== "stashed") {
|
|
1793
|
-
throw new Error(`Session is not stashed: ${session.status}`);
|
|
1794
|
-
}
|
|
1795
|
-
const existing = await getActiveSession(this.runtime, entityId, session.roomId);
|
|
1796
|
-
if (existing && existing.id !== sessionId) {
|
|
1797
|
-
throw new Error(`Active session already exists in room: ${existing.id}`);
|
|
1798
|
-
}
|
|
1799
|
-
session.status = "active";
|
|
1800
|
-
session.updatedAt = Date.now();
|
|
1801
|
-
session.expiresAt = this.calculateTTL(session);
|
|
1802
|
-
await saveSession(this.runtime, session);
|
|
1803
|
-
logger.debug(`[FormService] Restored session ${sessionId}`);
|
|
1804
|
-
return session;
|
|
1805
|
-
}
|
|
1806
|
-
async cancel(sessionId, entityId, force = false) {
|
|
1807
|
-
const session = await getSessionById(this.runtime, entityId, sessionId);
|
|
1808
|
-
if (!session) {
|
|
1809
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
1810
|
-
}
|
|
1811
|
-
if (!force && this.shouldConfirmCancel(session) && !session.cancelConfirmationAsked) {
|
|
1812
|
-
session.cancelConfirmationAsked = true;
|
|
1813
|
-
session.updatedAt = Date.now();
|
|
1814
|
-
await saveSession(this.runtime, session);
|
|
1815
|
-
return false;
|
|
1816
|
-
}
|
|
1817
|
-
const form = this.getForm(session.formId);
|
|
1818
|
-
session.status = "cancelled";
|
|
1819
|
-
session.updatedAt = Date.now();
|
|
1820
|
-
await saveSession(this.runtime, session);
|
|
1821
|
-
if (form?.hooks?.onCancel) {
|
|
1822
|
-
await this.executeHook(session, "onCancel");
|
|
1823
|
-
}
|
|
1824
|
-
logger.debug(`[FormService] Cancelled session ${sessionId}`);
|
|
1825
|
-
return true;
|
|
1826
|
-
}
|
|
1827
|
-
async getSubmissions(entityId, formId) {
|
|
1828
|
-
return getSubmissions(this.runtime, entityId, formId);
|
|
1829
|
-
}
|
|
1830
|
-
async getAutofill(entityId, formId) {
|
|
1831
|
-
const data = await getAutofillData(this.runtime, entityId, formId);
|
|
1832
|
-
return data?.values || null;
|
|
1833
|
-
}
|
|
1834
|
-
async applyAutofill(session) {
|
|
1835
|
-
const form = this.getForm(session.formId);
|
|
1836
|
-
if (!form?.ux?.allowAutofill) {
|
|
1837
|
-
return [];
|
|
1838
|
-
}
|
|
1839
|
-
const autofill = await getAutofillData(this.runtime, session.entityId, session.formId);
|
|
1840
|
-
if (!autofill) {
|
|
1841
|
-
return [];
|
|
1842
|
-
}
|
|
1843
|
-
const appliedFields = [];
|
|
1844
|
-
const now = Date.now();
|
|
1845
|
-
for (const control of form.controls) {
|
|
1846
|
-
if (session.fields[control.key]?.status !== "empty") {
|
|
1847
|
-
continue;
|
|
1848
|
-
}
|
|
1849
|
-
const value = autofill.values[control.key];
|
|
1850
|
-
if (value !== undefined) {
|
|
1851
|
-
session.fields[control.key] = {
|
|
1852
|
-
status: "filled",
|
|
1853
|
-
value,
|
|
1854
|
-
source: "autofill",
|
|
1855
|
-
updatedAt: now
|
|
1856
|
-
};
|
|
1857
|
-
appliedFields.push(control.key);
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
if (appliedFields.length > 0) {
|
|
1861
|
-
session.updatedAt = now;
|
|
1862
|
-
await saveSession(this.runtime, session);
|
|
1863
|
-
}
|
|
1864
|
-
return appliedFields;
|
|
1865
|
-
}
|
|
1866
|
-
getSessionContext(session) {
|
|
1867
|
-
const form = this.getForm(session.formId);
|
|
1868
|
-
if (!form) {
|
|
1869
|
-
return {
|
|
1870
|
-
hasActiveForm: false,
|
|
1871
|
-
progress: 0,
|
|
1872
|
-
filledFields: [],
|
|
1873
|
-
missingRequired: [],
|
|
1874
|
-
uncertainFields: [],
|
|
1875
|
-
nextField: null,
|
|
1876
|
-
pendingExternalFields: []
|
|
1877
|
-
};
|
|
1878
|
-
}
|
|
1879
|
-
const filledFields = [];
|
|
1880
|
-
const missingRequired = [];
|
|
1881
|
-
const uncertainFields = [];
|
|
1882
|
-
const pendingExternalFields = [];
|
|
1883
|
-
let nextField = null;
|
|
1884
|
-
let filledCount = 0;
|
|
1885
|
-
let totalRequired = 0;
|
|
1886
|
-
for (const control of form.controls) {
|
|
1887
|
-
if (control.hidden)
|
|
1888
|
-
continue;
|
|
1889
|
-
const fieldState = session.fields[control.key];
|
|
1890
|
-
if (control.required) {
|
|
1891
|
-
totalRequired++;
|
|
1892
|
-
}
|
|
1893
|
-
if (fieldState?.status === "filled") {
|
|
1894
|
-
filledCount++;
|
|
1895
|
-
filledFields.push({
|
|
1896
|
-
key: control.key,
|
|
1897
|
-
label: control.label,
|
|
1898
|
-
displayValue: formatValue(fieldState.value ?? null, control)
|
|
1899
|
-
});
|
|
1900
|
-
} else if (fieldState?.status === "pending") {
|
|
1901
|
-
if (fieldState.externalState) {
|
|
1902
|
-
pendingExternalFields.push({
|
|
1903
|
-
key: control.key,
|
|
1904
|
-
label: control.label,
|
|
1905
|
-
instructions: fieldState.externalState.instructions || "Waiting for confirmation...",
|
|
1906
|
-
reference: fieldState.externalState.reference || "",
|
|
1907
|
-
activatedAt: fieldState.externalState.activatedAt || Date.now(),
|
|
1908
|
-
address: fieldState.externalState.address
|
|
1909
|
-
});
|
|
1910
|
-
}
|
|
1911
|
-
} else if (fieldState?.status === "uncertain") {
|
|
1912
|
-
uncertainFields.push({
|
|
1913
|
-
key: control.key,
|
|
1914
|
-
label: control.label,
|
|
1915
|
-
value: fieldState.value ?? null,
|
|
1916
|
-
confidence: fieldState.confidence ?? 0
|
|
1917
|
-
});
|
|
1918
|
-
} else if (fieldState?.status === "invalid") {
|
|
1919
|
-
missingRequired.push({
|
|
1920
|
-
key: control.key,
|
|
1921
|
-
label: control.label,
|
|
1922
|
-
description: control.description,
|
|
1923
|
-
askPrompt: control.askPrompt
|
|
1924
|
-
});
|
|
1925
|
-
if (!nextField)
|
|
1926
|
-
nextField = control;
|
|
1927
|
-
} else if (control.required && fieldState?.status !== "skipped") {
|
|
1928
|
-
missingRequired.push({
|
|
1929
|
-
key: control.key,
|
|
1930
|
-
label: control.label,
|
|
1931
|
-
description: control.description,
|
|
1932
|
-
askPrompt: control.askPrompt
|
|
1933
|
-
});
|
|
1934
|
-
if (!nextField)
|
|
1935
|
-
nextField = control;
|
|
1936
|
-
} else if (!nextField && fieldState?.status === "empty") {
|
|
1937
|
-
nextField = control;
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
const progress = totalRequired > 0 ? Math.round(filledCount / totalRequired * 100) : 100;
|
|
1941
|
-
return {
|
|
1942
|
-
hasActiveForm: true,
|
|
1943
|
-
formId: session.formId,
|
|
1944
|
-
formName: form.name,
|
|
1945
|
-
progress,
|
|
1946
|
-
filledFields,
|
|
1947
|
-
missingRequired,
|
|
1948
|
-
uncertainFields,
|
|
1949
|
-
nextField,
|
|
1950
|
-
status: session.status,
|
|
1951
|
-
pendingCancelConfirmation: session.cancelConfirmationAsked && session.status === "active",
|
|
1952
|
-
pendingExternalFields
|
|
1953
|
-
};
|
|
1954
|
-
}
|
|
1955
|
-
getValues(session) {
|
|
1956
|
-
const values = {};
|
|
1957
|
-
for (const [key, state] of Object.entries(session.fields)) {
|
|
1958
|
-
if (state.value !== undefined) {
|
|
1959
|
-
values[key] = state.value;
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
return values;
|
|
1963
|
-
}
|
|
1964
|
-
getMappedValues(session) {
|
|
1965
|
-
const form = this.getForm(session.formId);
|
|
1966
|
-
if (!form)
|
|
1967
|
-
return {};
|
|
1968
|
-
const values = {};
|
|
1969
|
-
for (const control of form.controls) {
|
|
1970
|
-
const state = session.fields[control.key];
|
|
1971
|
-
if (state?.value !== undefined) {
|
|
1972
|
-
const key = control.dbbind || control.key;
|
|
1973
|
-
values[key] = state.value;
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
return values;
|
|
1977
|
-
}
|
|
1978
|
-
calculateTTL(session) {
|
|
1979
|
-
const form = this.getForm(session.formId);
|
|
1980
|
-
const config = form?.ttl || {};
|
|
1981
|
-
const minDays = config.minDays ?? 14;
|
|
1982
|
-
const maxDays = config.maxDays ?? 90;
|
|
1983
|
-
const multiplier = config.effortMultiplier ?? 0.5;
|
|
1984
|
-
const minutesSpent = session.effort.timeSpentMs / 60000;
|
|
1985
|
-
const effortDays = minutesSpent * multiplier;
|
|
1986
|
-
const ttlDays = Math.min(maxDays, Math.max(minDays, effortDays));
|
|
1987
|
-
return Date.now() + ttlDays * 24 * 60 * 60 * 1000;
|
|
1988
|
-
}
|
|
1989
|
-
shouldConfirmCancel(session) {
|
|
1990
|
-
const minEffortMs = 5 * 60 * 1000;
|
|
1991
|
-
return session.effort.timeSpentMs > minEffortMs;
|
|
1992
|
-
}
|
|
1993
|
-
async executeHook(session, hookName, options) {
|
|
1994
|
-
const form = this.getForm(session.formId);
|
|
1995
|
-
const workerName = form?.hooks?.[hookName];
|
|
1996
|
-
if (!workerName)
|
|
1997
|
-
return;
|
|
1998
|
-
const worker = this.runtime.getTaskWorker(workerName);
|
|
1999
|
-
if (!worker) {
|
|
2000
|
-
logger.warn(`[FormService] Hook worker not found: ${workerName}`);
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
try {
|
|
2004
|
-
const task = {
|
|
2005
|
-
id: session.id,
|
|
2006
|
-
name: workerName,
|
|
2007
|
-
roomId: session.roomId,
|
|
2008
|
-
entityId: session.entityId,
|
|
2009
|
-
tags: ["form-hook", hookName]
|
|
2010
|
-
};
|
|
2011
|
-
await worker.execute(this.runtime, {
|
|
2012
|
-
session,
|
|
2013
|
-
form,
|
|
2014
|
-
...options
|
|
2015
|
-
}, task);
|
|
2016
|
-
} catch (error) {
|
|
2017
|
-
logger.error(`[FormService] Hook execution failed: ${hookName}`, String(error));
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
checkAllRequiredFilled(session, form) {
|
|
2021
|
-
for (const control of form.controls) {
|
|
2022
|
-
if (!control.required)
|
|
2023
|
-
continue;
|
|
2024
|
-
const fieldState = session.fields[control.key];
|
|
2025
|
-
if (!fieldState || fieldState.status === "empty" || fieldState.status === "invalid") {
|
|
2026
|
-
return false;
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
return true;
|
|
2030
|
-
}
|
|
2031
|
-
};
|
|
2032
|
-
});
|
|
2033
|
-
|
|
2034
|
-
// src/actions/restore.ts
|
|
2035
|
-
var exports_restore = {};
|
|
2036
|
-
__export(exports_restore, {
|
|
2037
|
-
formRestoreAction: () => formRestoreAction,
|
|
2038
|
-
default: () => restore_default
|
|
2039
|
-
});
|
|
2040
1
|
import {
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
},
|
|
2091
|
-
handler: async (runtime, message, _state, _options, callback) => {
|
|
2092
|
-
try {
|
|
2093
|
-
const formService = runtime.getService("FORM");
|
|
2094
|
-
if (!formService) {
|
|
2095
|
-
await callback?.({
|
|
2096
|
-
text: "Sorry, I couldn't find the form service."
|
|
2097
|
-
});
|
|
2098
|
-
return { success: false };
|
|
2099
|
-
}
|
|
2100
|
-
const entityId = message.entityId;
|
|
2101
|
-
const roomId = message.roomId;
|
|
2102
|
-
if (!entityId || !roomId) {
|
|
2103
|
-
await callback?.({
|
|
2104
|
-
text: "Sorry, I couldn't identify you."
|
|
2105
|
-
});
|
|
2106
|
-
return { success: false };
|
|
2107
|
-
}
|
|
2108
|
-
const existing = await formService.getActiveSession(entityId, roomId);
|
|
2109
|
-
if (existing) {
|
|
2110
|
-
const form2 = formService.getForm(existing.formId);
|
|
2111
|
-
await callback?.({
|
|
2112
|
-
text: `You already have an active form: "${form2?.name || existing.formId}". Would you like to continue with that one, or should I save it and restore your other form?`
|
|
2113
|
-
});
|
|
2114
|
-
return { success: false };
|
|
2115
|
-
}
|
|
2116
|
-
const stashed = await formService.getStashedSessions(entityId);
|
|
2117
|
-
if (stashed.length === 0) {
|
|
2118
|
-
await callback?.({
|
|
2119
|
-
text: "You don't have any saved forms to resume."
|
|
2120
|
-
});
|
|
2121
|
-
return { success: false };
|
|
2122
|
-
}
|
|
2123
|
-
const sessionToRestore = stashed.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
2124
|
-
const session = await formService.restore(sessionToRestore.id, entityId);
|
|
2125
|
-
const form = formService.getForm(session.formId);
|
|
2126
|
-
const context = formService.getSessionContext(session);
|
|
2127
|
-
let responseText = `I've restored your "${form?.name || session.formId}" form. `;
|
|
2128
|
-
responseText += `You're ${context.progress}% complete. `;
|
|
2129
|
-
if (context.filledFields.length > 0) {
|
|
2130
|
-
responseText += `
|
|
2131
|
-
|
|
2132
|
-
Here's what I have so far:
|
|
2133
|
-
`;
|
|
2134
|
-
for (const field of context.filledFields) {
|
|
2135
|
-
responseText += `• ${field.label}: ${field.displayValue}
|
|
2136
|
-
`;
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
if (context.nextField) {
|
|
2140
|
-
responseText += `
|
|
2141
|
-
Let's continue with ${context.nextField.label}.`;
|
|
2142
|
-
if (context.nextField.askPrompt) {
|
|
2143
|
-
responseText += ` ${context.nextField.askPrompt}`;
|
|
2144
|
-
}
|
|
2145
|
-
} else if (context.status === "ready") {
|
|
2146
|
-
responseText += `
|
|
2147
|
-
Everything looks complete! Ready to submit?`;
|
|
2148
|
-
}
|
|
2149
|
-
await callback?.({
|
|
2150
|
-
text: responseText
|
|
2151
|
-
});
|
|
2152
|
-
return {
|
|
2153
|
-
success: true,
|
|
2154
|
-
data: {
|
|
2155
|
-
sessionId: session.id,
|
|
2156
|
-
formId: session.formId,
|
|
2157
|
-
progress: context.progress
|
|
2158
|
-
}
|
|
2159
|
-
};
|
|
2160
|
-
} catch (error) {
|
|
2161
|
-
logger2.error("[FormRestoreAction] Handler error:", String(error));
|
|
2162
|
-
await callback?.({
|
|
2163
|
-
text: "Sorry, I couldn't restore your form. Please try again."
|
|
2164
|
-
});
|
|
2165
|
-
return { success: false };
|
|
2166
|
-
}
|
|
2167
|
-
},
|
|
2168
|
-
examples: [
|
|
2169
|
-
[
|
|
2170
|
-
{
|
|
2171
|
-
name: "{{user1}}",
|
|
2172
|
-
content: { text: "Resume my form" }
|
|
2173
|
-
},
|
|
2174
|
-
{
|
|
2175
|
-
name: "{{agentName}}",
|
|
2176
|
-
content: {
|
|
2177
|
-
text: "I've restored your form. Let's continue where you left off."
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
],
|
|
2181
|
-
[
|
|
2182
|
-
{
|
|
2183
|
-
name: "{{user1}}",
|
|
2184
|
-
content: { text: "Continue with my registration" }
|
|
2185
|
-
},
|
|
2186
|
-
{
|
|
2187
|
-
name: "{{agentName}}",
|
|
2188
|
-
content: {
|
|
2189
|
-
text: "I've restored your Registration form. You're 60% complete."
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
],
|
|
2193
|
-
[
|
|
2194
|
-
{
|
|
2195
|
-
name: "{{user1}}",
|
|
2196
|
-
content: { text: "Pick up where I left off" }
|
|
2197
|
-
},
|
|
2198
|
-
{
|
|
2199
|
-
name: "{{agentName}}",
|
|
2200
|
-
content: {
|
|
2201
|
-
text: "I've restored your form. Here's what you have so far..."
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
]
|
|
2205
|
-
]
|
|
2206
|
-
};
|
|
2207
|
-
restore_default = formRestoreAction;
|
|
2208
|
-
});
|
|
2209
|
-
|
|
2210
|
-
// src/evaluators/extractor.ts
|
|
2211
|
-
var exports_extractor = {};
|
|
2212
|
-
__export(exports_extractor, {
|
|
2213
|
-
formEvaluator: () => formEvaluator,
|
|
2214
|
-
default: () => extractor_default
|
|
2215
|
-
});
|
|
2216
|
-
import { logger as logger3 } from "@elizaos/core";
|
|
2217
|
-
async function processExtractions(runtime, formService, session, form, entityId, extractions, messageId) {
|
|
2218
|
-
const updatedParents = new Set;
|
|
2219
|
-
for (const extraction of extractions) {
|
|
2220
|
-
if (extraction.field.includes(".")) {
|
|
2221
|
-
const [parentKey, subKey] = extraction.field.split(".");
|
|
2222
|
-
await formService.updateSubField(session.id, entityId, parentKey, subKey, extraction.value, extraction.confidence, messageId);
|
|
2223
|
-
await emitEvent(runtime, "FORM_SUBFIELD_UPDATED", {
|
|
2224
|
-
sessionId: session.id,
|
|
2225
|
-
parentField: parentKey,
|
|
2226
|
-
subField: subKey,
|
|
2227
|
-
value: extraction.value,
|
|
2228
|
-
confidence: extraction.confidence
|
|
2229
|
-
});
|
|
2230
|
-
updatedParents.add(parentKey);
|
|
2231
|
-
if (form.debug) {
|
|
2232
|
-
logger3.debug(`[FormEvaluator] Updated subfield ${parentKey}.${subKey}`);
|
|
2233
|
-
}
|
|
2234
|
-
} else {
|
|
2235
|
-
await formService.updateField(session.id, entityId, extraction.field, extraction.value, extraction.confidence, extraction.isCorrection ? "correction" : "extraction", messageId);
|
|
2236
|
-
await emitEvent(runtime, "FORM_FIELD_EXTRACTED", {
|
|
2237
|
-
sessionId: session.id,
|
|
2238
|
-
field: extraction.field,
|
|
2239
|
-
value: extraction.value,
|
|
2240
|
-
confidence: extraction.confidence
|
|
2241
|
-
});
|
|
2242
|
-
if (form.debug) {
|
|
2243
|
-
logger3.debug(`[FormEvaluator] Updated field ${extraction.field}`);
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
for (const parentKey of updatedParents) {
|
|
2248
|
-
await checkAndActivateExternalField(runtime, formService, session, form, entityId, parentKey);
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
async function checkAndActivateExternalField(runtime, formService, session, form, entityId, field) {
|
|
2252
|
-
const freshSession = await formService.getActiveSession(entityId, session.roomId);
|
|
2253
|
-
if (!freshSession)
|
|
2254
|
-
return;
|
|
2255
|
-
if (!formService.isExternalType(form.controls.find((c) => c.key === field)?.type || "")) {
|
|
2256
|
-
return;
|
|
2257
|
-
}
|
|
2258
|
-
if (!formService.areSubFieldsFilled(freshSession, field)) {
|
|
2259
|
-
return;
|
|
2260
|
-
}
|
|
2261
|
-
const subValues = formService.getSubFieldValues(freshSession, field);
|
|
2262
|
-
await emitEvent(runtime, "FORM_SUBCONTROLS_FILLED", {
|
|
2263
|
-
sessionId: session.id,
|
|
2264
|
-
field,
|
|
2265
|
-
subValues
|
|
2266
|
-
});
|
|
2267
|
-
logger3.debug(`[FormEvaluator] All subcontrols filled for ${field}, activating...`);
|
|
2268
|
-
try {
|
|
2269
|
-
const activation = await formService.activateExternalField(session.id, entityId, field);
|
|
2270
|
-
const activationPayload = JSON.parse(JSON.stringify(activation));
|
|
2271
|
-
await emitEvent(runtime, "FORM_EXTERNAL_ACTIVATED", {
|
|
2272
|
-
sessionId: session.id,
|
|
2273
|
-
field,
|
|
2274
|
-
activation: activationPayload
|
|
2275
|
-
});
|
|
2276
|
-
logger3.info(`[FormEvaluator] Activated external field ${field}: ${activation.instructions}`);
|
|
2277
|
-
} catch (error) {
|
|
2278
|
-
logger3.error(`[FormEvaluator] Failed to activate external field ${field}:`, String(error));
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
async function emitEvent(runtime, eventType, payload) {
|
|
2282
|
-
try {
|
|
2283
|
-
if (typeof runtime.emitEvent === "function") {
|
|
2284
|
-
const eventPayload = { runtime, ...payload };
|
|
2285
|
-
await runtime.emitEvent(eventType, eventPayload);
|
|
2286
|
-
}
|
|
2287
|
-
} catch (error) {
|
|
2288
|
-
logger3.debug(`[FormEvaluator] Event emission (${eventType}):`, String(error));
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
async function handleSubmit(formService, session, entityId) {
|
|
2292
|
-
try {
|
|
2293
|
-
await formService.submit(session.id, entityId);
|
|
2294
|
-
} catch (error) {
|
|
2295
|
-
logger3.debug("[FormEvaluator] Submit failed:", String(error));
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
async function handleUndo(formService, session, entityId, form) {
|
|
2299
|
-
if (!form.ux?.allowUndo) {
|
|
2300
|
-
return;
|
|
2301
|
-
}
|
|
2302
|
-
const result = await formService.undoLastChange(session.id, entityId);
|
|
2303
|
-
if (result) {
|
|
2304
|
-
logger3.debug("[FormEvaluator] Undid field:", result.field);
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
async function handleSkip(formService, session, entityId, form) {
|
|
2308
|
-
if (!form.ux?.allowSkip) {
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
if (session.lastAskedField) {
|
|
2312
|
-
const skipped = await formService.skipField(session.id, entityId, session.lastAskedField);
|
|
2313
|
-
if (skipped) {
|
|
2314
|
-
logger3.debug("[FormEvaluator] Skipped field:", session.lastAskedField);
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
var formEvaluator, extractor_default;
|
|
2319
|
-
var init_extractor = __esm(() => {
|
|
2320
|
-
init_extraction();
|
|
2321
|
-
init_template();
|
|
2322
|
-
formEvaluator = {
|
|
2323
|
-
name: "form_evaluator",
|
|
2324
|
-
description: "Extracts form fields and handles form intents from user messages",
|
|
2325
|
-
similes: ["FORM_EXTRACTION", "FORM_HANDLER"],
|
|
2326
|
-
examples: [],
|
|
2327
|
-
validate: async (runtime, message, _state) => {
|
|
2328
|
-
try {
|
|
2329
|
-
const formService = runtime.getService("FORM");
|
|
2330
|
-
if (!formService)
|
|
2331
|
-
return false;
|
|
2332
|
-
const entityId = message.entityId;
|
|
2333
|
-
const roomId = message.roomId;
|
|
2334
|
-
if (!entityId || !roomId)
|
|
2335
|
-
return false;
|
|
2336
|
-
const session = await formService.getActiveSession(entityId, roomId);
|
|
2337
|
-
const stashed = await formService.getStashedSessions(entityId);
|
|
2338
|
-
return session !== null || stashed.length > 0;
|
|
2339
|
-
} catch (error) {
|
|
2340
|
-
logger3.error("[FormEvaluator] Validation error:", String(error));
|
|
2341
|
-
return false;
|
|
2342
|
-
}
|
|
2343
|
-
},
|
|
2344
|
-
handler: async (runtime, message, _state) => {
|
|
2345
|
-
try {
|
|
2346
|
-
const formService = runtime.getService("FORM");
|
|
2347
|
-
if (!formService)
|
|
2348
|
-
return;
|
|
2349
|
-
const entityId = message.entityId;
|
|
2350
|
-
const roomId = message.roomId;
|
|
2351
|
-
const text = message.content?.text || "";
|
|
2352
|
-
if (!entityId || !roomId)
|
|
2353
|
-
return;
|
|
2354
|
-
if (!text.trim())
|
|
2355
|
-
return;
|
|
2356
|
-
let session = await formService.getActiveSession(entityId, roomId);
|
|
2357
|
-
let intent = quickIntentDetect(text);
|
|
2358
|
-
let extractions = [];
|
|
2359
|
-
if (intent === "restore" && !session) {
|
|
2360
|
-
logger3.debug("[FormEvaluator] Restore intent detected, deferring to action");
|
|
2361
|
-
return;
|
|
2362
|
-
}
|
|
2363
|
-
if (!session) {
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
const form = formService.getForm(session.formId);
|
|
2367
|
-
if (!form) {
|
|
2368
|
-
logger3.warn("[FormEvaluator] Form not found for session:", session.formId);
|
|
2369
|
-
return;
|
|
2370
|
-
}
|
|
2371
|
-
const templateValues = buildTemplateValues(session);
|
|
2372
|
-
if (!intent) {
|
|
2373
|
-
const result = await llmIntentAndExtract(runtime, text, form, form.controls, templateValues);
|
|
2374
|
-
intent = result.intent;
|
|
2375
|
-
extractions = result.extractions;
|
|
2376
|
-
if (form.debug) {
|
|
2377
|
-
logger3.debug("[FormEvaluator] LLM extraction result:", JSON.stringify({ intent, extractions }));
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
switch (intent) {
|
|
2381
|
-
case "submit":
|
|
2382
|
-
await handleSubmit(formService, session, entityId);
|
|
2383
|
-
break;
|
|
2384
|
-
case "stash":
|
|
2385
|
-
await formService.stash(session.id, entityId);
|
|
2386
|
-
break;
|
|
2387
|
-
case "cancel":
|
|
2388
|
-
await formService.cancel(session.id, entityId);
|
|
2389
|
-
break;
|
|
2390
|
-
case "undo":
|
|
2391
|
-
await handleUndo(formService, session, entityId, form);
|
|
2392
|
-
break;
|
|
2393
|
-
case "skip":
|
|
2394
|
-
await handleSkip(formService, session, entityId, form);
|
|
2395
|
-
break;
|
|
2396
|
-
case "autofill":
|
|
2397
|
-
await formService.applyAutofill(session);
|
|
2398
|
-
break;
|
|
2399
|
-
case "explain":
|
|
2400
|
-
case "example":
|
|
2401
|
-
case "progress":
|
|
2402
|
-
logger3.debug(`[FormEvaluator] Info intent: ${intent}`);
|
|
2403
|
-
break;
|
|
2404
|
-
case "restore":
|
|
2405
|
-
logger3.debug("[FormEvaluator] Restore intent - deferring to action");
|
|
2406
|
-
break;
|
|
2407
|
-
default:
|
|
2408
|
-
await processExtractions(runtime, formService, session, form, entityId, extractions, message.id);
|
|
2409
|
-
break;
|
|
2410
|
-
}
|
|
2411
|
-
session = await formService.getActiveSession(entityId, roomId);
|
|
2412
|
-
if (session) {
|
|
2413
|
-
session.lastMessageId = message.id;
|
|
2414
|
-
await formService.saveSession(session);
|
|
2415
|
-
}
|
|
2416
|
-
} catch (error) {
|
|
2417
|
-
logger3.error("[FormEvaluator] Handler error:", String(error));
|
|
2418
|
-
return;
|
|
2419
|
-
}
|
|
2420
|
-
return;
|
|
2421
|
-
}
|
|
2422
|
-
};
|
|
2423
|
-
extractor_default = formEvaluator;
|
|
2424
|
-
});
|
|
2425
|
-
|
|
2426
|
-
// src/providers/context.ts
|
|
2427
|
-
var exports_context = {};
|
|
2428
|
-
__export(exports_context, {
|
|
2429
|
-
formContextProvider: () => formContextProvider,
|
|
2430
|
-
default: () => context_default
|
|
2431
|
-
});
|
|
2432
|
-
import { logger as logger4 } from "@elizaos/core";
|
|
2433
|
-
var formContextProvider, context_default;
|
|
2434
|
-
var init_context = __esm(() => {
|
|
2435
|
-
init_template();
|
|
2436
|
-
formContextProvider = {
|
|
2437
|
-
name: "FORM_CONTEXT",
|
|
2438
|
-
description: "Provides context about active form sessions",
|
|
2439
|
-
dynamic: true,
|
|
2440
|
-
get: async (runtime, message, _state) => {
|
|
2441
|
-
try {
|
|
2442
|
-
const formService = runtime.getService("FORM");
|
|
2443
|
-
if (!formService) {
|
|
2444
|
-
return {
|
|
2445
|
-
data: { hasActiveForm: false },
|
|
2446
|
-
values: { formContext: "" },
|
|
2447
|
-
text: ""
|
|
2448
|
-
};
|
|
2449
|
-
}
|
|
2450
|
-
const entityId = message.entityId;
|
|
2451
|
-
const roomId = message.roomId;
|
|
2452
|
-
if (!entityId || !roomId) {
|
|
2453
|
-
return {
|
|
2454
|
-
data: { hasActiveForm: false },
|
|
2455
|
-
values: { formContext: "" },
|
|
2456
|
-
text: ""
|
|
2457
|
-
};
|
|
2458
|
-
}
|
|
2459
|
-
const session = await formService.getActiveSession(entityId, roomId);
|
|
2460
|
-
const stashed = await formService.getStashedSessions(entityId);
|
|
2461
|
-
if (!session && stashed.length === 0) {
|
|
2462
|
-
return {
|
|
2463
|
-
data: { hasActiveForm: false, stashedCount: 0 },
|
|
2464
|
-
values: { formContext: "" },
|
|
2465
|
-
text: ""
|
|
2466
|
-
};
|
|
2467
|
-
}
|
|
2468
|
-
let contextText = "";
|
|
2469
|
-
let contextState;
|
|
2470
|
-
if (session) {
|
|
2471
|
-
contextState = formService.getSessionContext(session);
|
|
2472
|
-
const form = formService.getForm(session.formId);
|
|
2473
|
-
const templateValues = buildTemplateValues(session);
|
|
2474
|
-
const resolveText = (value) => renderTemplate(value, templateValues);
|
|
2475
|
-
contextState = {
|
|
2476
|
-
...contextState,
|
|
2477
|
-
filledFields: contextState.filledFields.map((field) => ({
|
|
2478
|
-
...field,
|
|
2479
|
-
label: resolveText(field.label) ?? field.label
|
|
2480
|
-
})),
|
|
2481
|
-
missingRequired: contextState.missingRequired.map((field) => ({
|
|
2482
|
-
...field,
|
|
2483
|
-
label: resolveText(field.label) ?? field.label,
|
|
2484
|
-
description: resolveText(field.description),
|
|
2485
|
-
askPrompt: resolveText(field.askPrompt)
|
|
2486
|
-
})),
|
|
2487
|
-
uncertainFields: contextState.uncertainFields.map((field) => ({
|
|
2488
|
-
...field,
|
|
2489
|
-
label: resolveText(field.label) ?? field.label
|
|
2490
|
-
})),
|
|
2491
|
-
nextField: contextState.nextField ? resolveControlTemplates(contextState.nextField, templateValues) : null
|
|
2492
|
-
};
|
|
2493
|
-
contextText = `# Active Form: ${form?.name || session.formId}
|
|
2494
|
-
|
|
2495
|
-
`;
|
|
2496
|
-
contextText += `Progress: ${contextState.progress}%
|
|
2497
|
-
|
|
2498
|
-
`;
|
|
2499
|
-
if (contextState.filledFields.length > 0) {
|
|
2500
|
-
contextText += `## Collected Information
|
|
2501
|
-
`;
|
|
2502
|
-
for (const field of contextState.filledFields) {
|
|
2503
|
-
contextText += `- ${field.label}: ${field.displayValue}
|
|
2504
|
-
`;
|
|
2505
|
-
}
|
|
2506
|
-
contextText += `
|
|
2507
|
-
`;
|
|
2508
|
-
}
|
|
2509
|
-
if (contextState.missingRequired.length > 0) {
|
|
2510
|
-
contextText += `## Still Needed
|
|
2511
|
-
`;
|
|
2512
|
-
for (const field of contextState.missingRequired) {
|
|
2513
|
-
contextText += `- ${field.label}${field.description ? ` (${field.description})` : ""}
|
|
2514
|
-
`;
|
|
2515
|
-
}
|
|
2516
|
-
contextText += `
|
|
2517
|
-
`;
|
|
2518
|
-
}
|
|
2519
|
-
if (contextState.uncertainFields.length > 0) {
|
|
2520
|
-
contextText += `## Needs Confirmation
|
|
2521
|
-
`;
|
|
2522
|
-
for (const field of contextState.uncertainFields) {
|
|
2523
|
-
contextText += `- ${field.label}: "${field.value}" (${Math.round(field.confidence * 100)}% confident)
|
|
2524
|
-
`;
|
|
2525
|
-
}
|
|
2526
|
-
contextText += `
|
|
2527
|
-
`;
|
|
2528
|
-
}
|
|
2529
|
-
if (contextState.pendingExternalFields.length > 0) {
|
|
2530
|
-
contextText += `## Waiting For External Action
|
|
2531
|
-
`;
|
|
2532
|
-
for (const field of contextState.pendingExternalFields) {
|
|
2533
|
-
const ageMs = Date.now() - field.activatedAt;
|
|
2534
|
-
const ageMin = Math.floor(ageMs / 60000);
|
|
2535
|
-
const ageText = ageMin < 1 ? "just now" : `${ageMin}m ago`;
|
|
2536
|
-
contextText += `- ${field.label}: ${field.instructions} (started ${ageText})
|
|
2537
|
-
`;
|
|
2538
|
-
if (field.address) {
|
|
2539
|
-
contextText += ` Address: ${field.address}
|
|
2540
|
-
`;
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
contextText += `
|
|
2544
|
-
`;
|
|
2545
|
-
}
|
|
2546
|
-
contextText += `## Agent Guidance
|
|
2547
|
-
`;
|
|
2548
|
-
if (contextState.pendingExternalFields.length > 0) {
|
|
2549
|
-
const pending = contextState.pendingExternalFields[0];
|
|
2550
|
-
contextText += `Waiting for external action. Remind user: "${pending.instructions}"
|
|
2551
|
-
`;
|
|
2552
|
-
} else if (contextState.pendingCancelConfirmation) {
|
|
2553
|
-
contextText += `User is trying to cancel. Confirm: "You've spent time on this. Are you sure you want to cancel?"
|
|
2554
|
-
`;
|
|
2555
|
-
} else if (contextState.uncertainFields.length > 0) {
|
|
2556
|
-
const uncertain = contextState.uncertainFields[0];
|
|
2557
|
-
contextText += `Ask user to confirm: "I understood your ${uncertain.label} as '${uncertain.value}'. Is that correct?"
|
|
2558
|
-
`;
|
|
2559
|
-
} else if (contextState.nextField) {
|
|
2560
|
-
const next = contextState.nextField;
|
|
2561
|
-
const prompt = next.askPrompt || `Ask for their ${next.label}`;
|
|
2562
|
-
contextText += `Next: ${prompt}
|
|
2563
|
-
`;
|
|
2564
|
-
if (next.example) {
|
|
2565
|
-
contextText += `Example: "${next.example}"
|
|
2566
|
-
`;
|
|
2567
|
-
}
|
|
2568
|
-
} else if (contextState.status === "ready") {
|
|
2569
|
-
contextText += `All fields collected! Nudge user to submit: "I have everything I need. Ready to submit?"
|
|
2570
|
-
`;
|
|
2571
|
-
}
|
|
2572
|
-
contextText += `
|
|
2573
|
-
`;
|
|
2574
|
-
contextText += `## User Can Say
|
|
2575
|
-
`;
|
|
2576
|
-
contextText += `- Provide information for any field
|
|
2577
|
-
`;
|
|
2578
|
-
contextText += `- "undo" or "go back" to revert last change
|
|
2579
|
-
`;
|
|
2580
|
-
contextText += `- "skip" to skip optional fields
|
|
2581
|
-
`;
|
|
2582
|
-
contextText += `- "why?" to get explanation about a field
|
|
2583
|
-
`;
|
|
2584
|
-
contextText += `- "how far?" to check progress
|
|
2585
|
-
`;
|
|
2586
|
-
contextText += `- "submit" or "done" when ready
|
|
2587
|
-
`;
|
|
2588
|
-
contextText += `- "save for later" to stash the form
|
|
2589
|
-
`;
|
|
2590
|
-
contextText += `- "cancel" to abandon the form
|
|
2591
|
-
`;
|
|
2592
|
-
} else {
|
|
2593
|
-
contextState = {
|
|
2594
|
-
hasActiveForm: false,
|
|
2595
|
-
progress: 0,
|
|
2596
|
-
filledFields: [],
|
|
2597
|
-
missingRequired: [],
|
|
2598
|
-
uncertainFields: [],
|
|
2599
|
-
nextField: null,
|
|
2600
|
-
stashedCount: stashed.length,
|
|
2601
|
-
pendingExternalFields: []
|
|
2602
|
-
};
|
|
2603
|
-
}
|
|
2604
|
-
if (stashed.length > 0) {
|
|
2605
|
-
contextText += `
|
|
2606
|
-
## Saved Forms
|
|
2607
|
-
`;
|
|
2608
|
-
contextText += `User has ${stashed.length} saved form(s). They can say "resume" or "continue" to restore one.
|
|
2609
|
-
`;
|
|
2610
|
-
for (const s of stashed) {
|
|
2611
|
-
const form = formService.getForm(s.formId);
|
|
2612
|
-
const ctx = formService.getSessionContext(s);
|
|
2613
|
-
contextText += `- ${form?.name || s.formId} (${ctx.progress}% complete)
|
|
2614
|
-
`;
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
return {
|
|
2618
|
-
data: JSON.parse(JSON.stringify(contextState)),
|
|
2619
|
-
values: {
|
|
2620
|
-
formContext: contextText,
|
|
2621
|
-
hasActiveForm: String(contextState.hasActiveForm),
|
|
2622
|
-
formProgress: String(contextState.progress),
|
|
2623
|
-
formStatus: contextState.status || "",
|
|
2624
|
-
stashedCount: String(stashed.length)
|
|
2625
|
-
},
|
|
2626
|
-
text: contextText
|
|
2627
|
-
};
|
|
2628
|
-
} catch (error) {
|
|
2629
|
-
logger4.error("[FormContextProvider] Error:", String(error));
|
|
2630
|
-
return {
|
|
2631
|
-
data: { hasActiveForm: false, error: true },
|
|
2632
|
-
values: { formContext: "Error loading form context." },
|
|
2633
|
-
text: "Error loading form context."
|
|
2634
|
-
};
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
};
|
|
2638
|
-
context_default = formContextProvider;
|
|
2639
|
-
});
|
|
2640
|
-
|
|
2641
|
-
// index.ts
|
|
2642
|
-
init_builtins();
|
|
2643
|
-
init_validation();
|
|
2644
|
-
init_storage();
|
|
2645
|
-
init_extraction();
|
|
2646
|
-
init_types();
|
|
2
|
+
BUILTIN_TYPES,
|
|
3
|
+
BUILTIN_TYPE_MAP,
|
|
4
|
+
FORM_AUTOFILL_COMPONENT,
|
|
5
|
+
FORM_CONTROL_DEFAULTS,
|
|
6
|
+
FORM_DEFINITION_DEFAULTS,
|
|
7
|
+
FORM_SESSION_COMPONENT,
|
|
8
|
+
FORM_SUBMISSION_COMPONENT,
|
|
9
|
+
FormService,
|
|
10
|
+
deleteSession,
|
|
11
|
+
getActiveSession,
|
|
12
|
+
getAllActiveSessions,
|
|
13
|
+
getAutofillData,
|
|
14
|
+
getBuiltinType,
|
|
15
|
+
getStashedSessions,
|
|
16
|
+
getSubmissions,
|
|
17
|
+
isBuiltinType,
|
|
18
|
+
registerBuiltinTypes,
|
|
19
|
+
saveAutofillData,
|
|
20
|
+
saveSession,
|
|
21
|
+
saveSubmission
|
|
22
|
+
} from "./chunk-R4VBS2YK.js";
|
|
23
|
+
import {
|
|
24
|
+
formRestoreAction
|
|
25
|
+
} from "./chunk-TBCL2ILB.js";
|
|
26
|
+
import {
|
|
27
|
+
detectCorrection,
|
|
28
|
+
extractSingleField,
|
|
29
|
+
formEvaluator,
|
|
30
|
+
llmIntentAndExtract
|
|
31
|
+
} from "./chunk-XHECCAUT.js";
|
|
32
|
+
import {
|
|
33
|
+
clearTypeHandlers,
|
|
34
|
+
formatValue,
|
|
35
|
+
getTypeHandler,
|
|
36
|
+
matchesMimeType,
|
|
37
|
+
parseValue,
|
|
38
|
+
registerTypeHandler,
|
|
39
|
+
validateField
|
|
40
|
+
} from "./chunk-ARWZY3NX.js";
|
|
41
|
+
import {
|
|
42
|
+
hasDataToExtract,
|
|
43
|
+
isLifecycleIntent,
|
|
44
|
+
isUXIntent,
|
|
45
|
+
quickIntentDetect
|
|
46
|
+
} from "./chunk-YTWANJ3R.js";
|
|
47
|
+
import {
|
|
48
|
+
formContextProvider
|
|
49
|
+
} from "./chunk-4B5QLNVA.js";
|
|
50
|
+
import "./chunk-WY4WK3HD.js";
|
|
2647
51
|
|
|
2648
52
|
// src/ttl.ts
|
|
2649
|
-
init_types();
|
|
2650
53
|
function calculateTTL(session, form) {
|
|
2651
54
|
const config = form?.ttl || {};
|
|
2652
55
|
const minDays = config.minDays ?? FORM_DEFINITION_DEFAULTS.ttl.minDays;
|
|
2653
56
|
const maxDays = config.maxDays ?? FORM_DEFINITION_DEFAULTS.ttl.maxDays;
|
|
2654
57
|
const multiplier = config.effortMultiplier ?? FORM_DEFINITION_DEFAULTS.ttl.effortMultiplier;
|
|
2655
|
-
const minutesSpent = session.effort.timeSpentMs /
|
|
58
|
+
const minutesSpent = session.effort.timeSpentMs / 6e4;
|
|
2656
59
|
const effortDays = minutesSpent * multiplier;
|
|
2657
60
|
const ttlDays = Math.min(maxDays, Math.max(minDays, effortDays));
|
|
2658
|
-
return Date.now() + ttlDays * 24 * 60 * 60 *
|
|
61
|
+
return Date.now() + ttlDays * 24 * 60 * 60 * 1e3;
|
|
2659
62
|
}
|
|
2660
63
|
function shouldNudge(session, form) {
|
|
2661
64
|
const nudgeConfig = form?.nudge;
|
|
@@ -2667,14 +70,14 @@ function shouldNudge(session, form) {
|
|
|
2667
70
|
return false;
|
|
2668
71
|
}
|
|
2669
72
|
const afterInactiveHours = nudgeConfig?.afterInactiveHours ?? FORM_DEFINITION_DEFAULTS.nudge.afterInactiveHours;
|
|
2670
|
-
const inactiveMs = afterInactiveHours * 60 * 60 *
|
|
73
|
+
const inactiveMs = afterInactiveHours * 60 * 60 * 1e3;
|
|
2671
74
|
const timeSinceInteraction = Date.now() - session.effort.lastInteractionAt;
|
|
2672
75
|
if (timeSinceInteraction < inactiveMs) {
|
|
2673
76
|
return false;
|
|
2674
77
|
}
|
|
2675
78
|
if (session.lastNudgeAt) {
|
|
2676
79
|
const timeSinceNudge = Date.now() - session.lastNudgeAt;
|
|
2677
|
-
if (timeSinceNudge < 24 * 60 * 60 *
|
|
80
|
+
if (timeSinceNudge < 24 * 60 * 60 * 1e3) {
|
|
2678
81
|
return false;
|
|
2679
82
|
}
|
|
2680
83
|
}
|
|
@@ -2687,7 +90,7 @@ function isExpired(session) {
|
|
|
2687
90
|
return session.expiresAt < Date.now();
|
|
2688
91
|
}
|
|
2689
92
|
function shouldConfirmCancel(session) {
|
|
2690
|
-
const minEffortMs = 5 * 60 *
|
|
93
|
+
const minEffortMs = 5 * 60 * 1e3;
|
|
2691
94
|
return session.effort.timeSpentMs > minEffortMs;
|
|
2692
95
|
}
|
|
2693
96
|
function formatTimeRemaining(session) {
|
|
@@ -2695,7 +98,7 @@ function formatTimeRemaining(session) {
|
|
|
2695
98
|
if (remaining <= 0) {
|
|
2696
99
|
return "expired";
|
|
2697
100
|
}
|
|
2698
|
-
const hours = Math.floor(remaining / (60 * 60 *
|
|
101
|
+
const hours = Math.floor(remaining / (60 * 60 * 1e3));
|
|
2699
102
|
const days = Math.floor(hours / 24);
|
|
2700
103
|
if (days > 0) {
|
|
2701
104
|
return `${days} day${days > 1 ? "s" : ""}`;
|
|
@@ -2703,11 +106,11 @@ function formatTimeRemaining(session) {
|
|
|
2703
106
|
if (hours > 0) {
|
|
2704
107
|
return `${hours} hour${hours > 1 ? "s" : ""}`;
|
|
2705
108
|
}
|
|
2706
|
-
const minutes = Math.floor(remaining / (60 *
|
|
109
|
+
const minutes = Math.floor(remaining / (60 * 1e3));
|
|
2707
110
|
return `${minutes} minute${minutes > 1 ? "s" : ""}`;
|
|
2708
111
|
}
|
|
2709
112
|
function formatEffort(session) {
|
|
2710
|
-
const minutes = Math.floor(session.effort.timeSpentMs /
|
|
113
|
+
const minutes = Math.floor(session.effort.timeSpentMs / 6e4);
|
|
2711
114
|
if (minutes < 1) {
|
|
2712
115
|
return "just started";
|
|
2713
116
|
}
|
|
@@ -2721,25 +124,41 @@ function formatEffort(session) {
|
|
|
2721
124
|
}
|
|
2722
125
|
return `${hours}h ${remainingMinutes}m`;
|
|
2723
126
|
}
|
|
127
|
+
|
|
2724
128
|
// src/defaults.ts
|
|
2725
|
-
init_types();
|
|
2726
129
|
function applyControlDefaults(control) {
|
|
2727
130
|
return {
|
|
131
|
+
// Required field (must be present)
|
|
2728
132
|
key: control.key,
|
|
133
|
+
// Derive label from key if not provided
|
|
134
|
+
// WHY: User sees labels, default should be readable
|
|
2729
135
|
label: control.label || prettify(control.key),
|
|
136
|
+
// Default type is text (most common)
|
|
2730
137
|
type: control.type || FORM_CONTROL_DEFAULTS.type,
|
|
138
|
+
// Default not required (explicit opt-in)
|
|
139
|
+
// WHY: Safer to require opt-in for required fields
|
|
2731
140
|
required: control.required ?? FORM_CONTROL_DEFAULTS.required,
|
|
141
|
+
// Default confidence threshold for auto-acceptance
|
|
142
|
+
// WHY 0.8: High enough to be confident, low enough to be useful
|
|
2732
143
|
confirmThreshold: control.confirmThreshold ?? FORM_CONTROL_DEFAULTS.confirmThreshold,
|
|
144
|
+
// Spread remaining properties (override defaults)
|
|
2733
145
|
...control
|
|
2734
146
|
};
|
|
2735
147
|
}
|
|
2736
148
|
function applyFormDefaults(form) {
|
|
2737
149
|
return {
|
|
150
|
+
// Required fields
|
|
2738
151
|
id: form.id,
|
|
152
|
+
// Derive name from id if not provided
|
|
2739
153
|
name: form.name || prettify(form.id),
|
|
154
|
+
// Default version for schema tracking
|
|
2740
155
|
version: form.version ?? FORM_DEFINITION_DEFAULTS.version,
|
|
156
|
+
// Default status is active
|
|
2741
157
|
status: form.status ?? FORM_DEFINITION_DEFAULTS.status,
|
|
158
|
+
// Apply defaults to all controls
|
|
2742
159
|
controls: (form.controls || []).map(applyControlDefaults),
|
|
160
|
+
// UX defaults - enable helpful features by default
|
|
161
|
+
// WHY enable by default: Better user experience out of the box
|
|
2743
162
|
ux: {
|
|
2744
163
|
allowUndo: form.ux?.allowUndo ?? FORM_DEFINITION_DEFAULTS.ux.allowUndo,
|
|
2745
164
|
allowSkip: form.ux?.allowSkip ?? FORM_DEFINITION_DEFAULTS.ux.allowSkip,
|
|
@@ -2748,190 +167,266 @@ function applyFormDefaults(form) {
|
|
|
2748
167
|
showExplanations: form.ux?.showExplanations ?? FORM_DEFINITION_DEFAULTS.ux.showExplanations,
|
|
2749
168
|
allowAutofill: form.ux?.allowAutofill ?? FORM_DEFINITION_DEFAULTS.ux.allowAutofill
|
|
2750
169
|
},
|
|
170
|
+
// TTL defaults - generous retention
|
|
171
|
+
// WHY generous: Better to keep data too long than lose user work
|
|
2751
172
|
ttl: {
|
|
2752
173
|
minDays: form.ttl?.minDays ?? FORM_DEFINITION_DEFAULTS.ttl.minDays,
|
|
2753
174
|
maxDays: form.ttl?.maxDays ?? FORM_DEFINITION_DEFAULTS.ttl.maxDays,
|
|
2754
175
|
effortMultiplier: form.ttl?.effortMultiplier ?? FORM_DEFINITION_DEFAULTS.ttl.effortMultiplier
|
|
2755
176
|
},
|
|
177
|
+
// Nudge defaults - helpful but not annoying
|
|
2756
178
|
nudge: {
|
|
2757
179
|
enabled: form.nudge?.enabled ?? FORM_DEFINITION_DEFAULTS.nudge.enabled,
|
|
2758
180
|
afterInactiveHours: form.nudge?.afterInactiveHours ?? FORM_DEFINITION_DEFAULTS.nudge.afterInactiveHours,
|
|
2759
181
|
maxNudges: form.nudge?.maxNudges ?? FORM_DEFINITION_DEFAULTS.nudge.maxNudges,
|
|
2760
182
|
message: form.nudge?.message
|
|
2761
183
|
},
|
|
184
|
+
// Debug defaults to off for performance
|
|
2762
185
|
debug: form.debug ?? FORM_DEFINITION_DEFAULTS.debug,
|
|
186
|
+
// Spread remaining properties (override defaults)
|
|
2763
187
|
...form
|
|
2764
188
|
};
|
|
2765
189
|
}
|
|
2766
190
|
function prettify(key) {
|
|
2767
191
|
return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2768
192
|
}
|
|
193
|
+
|
|
2769
194
|
// src/builder.ts
|
|
2770
|
-
|
|
195
|
+
var ControlBuilder = class _ControlBuilder {
|
|
196
|
+
/** Partial control being built */
|
|
2771
197
|
control;
|
|
198
|
+
/**
|
|
199
|
+
* Create a new ControlBuilder.
|
|
200
|
+
*
|
|
201
|
+
* @param key - The unique key for this control
|
|
202
|
+
*/
|
|
2772
203
|
constructor(key) {
|
|
2773
204
|
this.control = { key };
|
|
2774
205
|
}
|
|
206
|
+
// ═══ STATIC FACTORIES ═══
|
|
207
|
+
// WHY static factories: Cleaner than `new ControlBuilder(key).type('text')`
|
|
208
|
+
/** Create a generic field builder */
|
|
2775
209
|
static field(key) {
|
|
2776
|
-
return new
|
|
210
|
+
return new _ControlBuilder(key);
|
|
2777
211
|
}
|
|
212
|
+
/** Create a text field */
|
|
2778
213
|
static text(key) {
|
|
2779
|
-
return new
|
|
214
|
+
return new _ControlBuilder(key).type("text");
|
|
2780
215
|
}
|
|
216
|
+
/** Create an email field */
|
|
2781
217
|
static email(key) {
|
|
2782
|
-
return new
|
|
218
|
+
return new _ControlBuilder(key).type("email");
|
|
2783
219
|
}
|
|
220
|
+
/** Create a number field */
|
|
2784
221
|
static number(key) {
|
|
2785
|
-
return new
|
|
222
|
+
return new _ControlBuilder(key).type("number");
|
|
2786
223
|
}
|
|
224
|
+
/** Create a boolean (yes/no) field */
|
|
2787
225
|
static boolean(key) {
|
|
2788
|
-
return new
|
|
226
|
+
return new _ControlBuilder(key).type("boolean");
|
|
2789
227
|
}
|
|
228
|
+
/** Create a select field with options */
|
|
2790
229
|
static select(key, options) {
|
|
2791
|
-
return new
|
|
230
|
+
return new _ControlBuilder(key).type("select").options(options);
|
|
2792
231
|
}
|
|
232
|
+
/** Create a date field */
|
|
2793
233
|
static date(key) {
|
|
2794
|
-
return new
|
|
234
|
+
return new _ControlBuilder(key).type("date");
|
|
2795
235
|
}
|
|
236
|
+
/** Create a file upload field */
|
|
2796
237
|
static file(key) {
|
|
2797
|
-
return new
|
|
238
|
+
return new _ControlBuilder(key).type("file");
|
|
2798
239
|
}
|
|
240
|
+
// ═══ TYPE ═══
|
|
241
|
+
/** Set the field type */
|
|
2799
242
|
type(type) {
|
|
2800
243
|
this.control.type = type;
|
|
2801
244
|
return this;
|
|
2802
245
|
}
|
|
246
|
+
// ═══ BEHAVIOR ═══
|
|
247
|
+
/** Mark field as required */
|
|
2803
248
|
required() {
|
|
2804
249
|
this.control.required = true;
|
|
2805
250
|
return this;
|
|
2806
251
|
}
|
|
252
|
+
/** Mark field as optional (default) */
|
|
2807
253
|
optional() {
|
|
2808
254
|
this.control.required = false;
|
|
2809
255
|
return this;
|
|
2810
256
|
}
|
|
257
|
+
/** Mark field as hidden (extract silently, never ask) */
|
|
2811
258
|
hidden() {
|
|
2812
259
|
this.control.hidden = true;
|
|
2813
260
|
return this;
|
|
2814
261
|
}
|
|
262
|
+
/** Mark field as sensitive (don't echo value back) */
|
|
2815
263
|
sensitive() {
|
|
2816
264
|
this.control.sensitive = true;
|
|
2817
265
|
return this;
|
|
2818
266
|
}
|
|
267
|
+
/** Mark field as readonly (can't change after set) */
|
|
2819
268
|
readonly() {
|
|
2820
269
|
this.control.readonly = true;
|
|
2821
270
|
return this;
|
|
2822
271
|
}
|
|
272
|
+
/** Mark field as accepting multiple values */
|
|
2823
273
|
multiple() {
|
|
2824
274
|
this.control.multiple = true;
|
|
2825
275
|
return this;
|
|
2826
276
|
}
|
|
277
|
+
// ═══ VALIDATION ═══
|
|
278
|
+
/** Set regex pattern for validation */
|
|
2827
279
|
pattern(regex) {
|
|
2828
280
|
this.control.pattern = regex;
|
|
2829
281
|
return this;
|
|
2830
282
|
}
|
|
283
|
+
/** Set minimum value (for numbers) or minimum length (via minLength) */
|
|
2831
284
|
min(n) {
|
|
2832
285
|
this.control.min = n;
|
|
2833
286
|
return this;
|
|
2834
287
|
}
|
|
288
|
+
/** Set maximum value (for numbers) or maximum length (via maxLength) */
|
|
2835
289
|
max(n) {
|
|
2836
290
|
this.control.max = n;
|
|
2837
291
|
return this;
|
|
2838
292
|
}
|
|
293
|
+
/** Set minimum string length */
|
|
2839
294
|
minLength(n) {
|
|
2840
295
|
this.control.minLength = n;
|
|
2841
296
|
return this;
|
|
2842
297
|
}
|
|
298
|
+
/** Set maximum string length */
|
|
2843
299
|
maxLength(n) {
|
|
2844
300
|
this.control.maxLength = n;
|
|
2845
301
|
return this;
|
|
2846
302
|
}
|
|
303
|
+
/** Set allowed values (enum) */
|
|
2847
304
|
enum(values) {
|
|
2848
305
|
this.control.enum = values;
|
|
2849
306
|
return this;
|
|
2850
307
|
}
|
|
308
|
+
/** Set select options */
|
|
2851
309
|
options(opts) {
|
|
2852
310
|
this.control.options = opts;
|
|
2853
311
|
return this;
|
|
2854
312
|
}
|
|
313
|
+
// ═══ AGENT HINTS ═══
|
|
314
|
+
// WHY agent hints: Help LLM extract values correctly
|
|
315
|
+
/** Set human-readable label */
|
|
2855
316
|
label(label) {
|
|
2856
317
|
this.control.label = label;
|
|
2857
318
|
return this;
|
|
2858
319
|
}
|
|
320
|
+
/** Set custom prompt for asking this field */
|
|
2859
321
|
ask(prompt) {
|
|
2860
322
|
this.control.askPrompt = prompt;
|
|
2861
323
|
return this;
|
|
2862
324
|
}
|
|
325
|
+
/** Set description for LLM context */
|
|
2863
326
|
description(desc) {
|
|
2864
327
|
this.control.description = desc;
|
|
2865
328
|
return this;
|
|
2866
329
|
}
|
|
330
|
+
/** Add extraction hints (keywords to look for) */
|
|
2867
331
|
hint(...hints) {
|
|
2868
332
|
this.control.extractHints = hints;
|
|
2869
333
|
return this;
|
|
2870
334
|
}
|
|
335
|
+
/** Set example value for "give me an example" */
|
|
2871
336
|
example(value) {
|
|
2872
337
|
this.control.example = value;
|
|
2873
338
|
return this;
|
|
2874
339
|
}
|
|
340
|
+
/** Set confidence threshold for auto-acceptance */
|
|
2875
341
|
confirmThreshold(n) {
|
|
2876
342
|
this.control.confirmThreshold = n;
|
|
2877
343
|
return this;
|
|
2878
344
|
}
|
|
345
|
+
// ═══ FILE OPTIONS ═══
|
|
346
|
+
/** Set accepted MIME types for file upload */
|
|
2879
347
|
accept(mimeTypes) {
|
|
2880
348
|
this.control.file = { ...this.control.file, accept: mimeTypes };
|
|
2881
349
|
return this;
|
|
2882
350
|
}
|
|
351
|
+
/** Set maximum file size in bytes */
|
|
2883
352
|
maxSize(bytes) {
|
|
2884
353
|
this.control.file = { ...this.control.file, maxSize: bytes };
|
|
2885
354
|
return this;
|
|
2886
355
|
}
|
|
356
|
+
/** Set maximum number of files */
|
|
2887
357
|
maxFiles(n) {
|
|
2888
358
|
this.control.file = { ...this.control.file, maxFiles: n };
|
|
2889
359
|
return this;
|
|
2890
360
|
}
|
|
361
|
+
// ═══ ACCESS ═══
|
|
362
|
+
/** Set roles that can see/fill this field */
|
|
2891
363
|
roles(...roles) {
|
|
2892
364
|
this.control.roles = roles;
|
|
2893
365
|
return this;
|
|
2894
366
|
}
|
|
367
|
+
// ═══ DEFAULTS & CONDITIONS ═══
|
|
368
|
+
/** Set default value */
|
|
2895
369
|
default(value) {
|
|
2896
370
|
this.control.defaultValue = value;
|
|
2897
371
|
return this;
|
|
2898
372
|
}
|
|
373
|
+
/** Set dependency on another field */
|
|
2899
374
|
dependsOn(field, condition = "exists", value) {
|
|
2900
375
|
this.control.dependsOn = { field, condition, value };
|
|
2901
376
|
return this;
|
|
2902
377
|
}
|
|
378
|
+
// ═══ DATABASE ═══
|
|
379
|
+
/** Set database column name (defaults to key) */
|
|
2903
380
|
dbbind(columnName) {
|
|
2904
381
|
this.control.dbbind = columnName;
|
|
2905
382
|
return this;
|
|
2906
383
|
}
|
|
384
|
+
// ═══ UI ═══
|
|
385
|
+
/** Set section name for grouping */
|
|
2907
386
|
section(name) {
|
|
2908
387
|
this.control.ui = { ...this.control.ui, section: name };
|
|
2909
388
|
return this;
|
|
2910
389
|
}
|
|
390
|
+
/** Set display order within section */
|
|
2911
391
|
order(n) {
|
|
2912
392
|
this.control.ui = { ...this.control.ui, order: n };
|
|
2913
393
|
return this;
|
|
2914
394
|
}
|
|
395
|
+
/** Set placeholder text */
|
|
2915
396
|
placeholder(text) {
|
|
2916
397
|
this.control.ui = { ...this.control.ui, placeholder: text };
|
|
2917
398
|
return this;
|
|
2918
399
|
}
|
|
400
|
+
/** Set help text */
|
|
2919
401
|
helpText(text) {
|
|
2920
402
|
this.control.ui = { ...this.control.ui, helpText: text };
|
|
2921
403
|
return this;
|
|
2922
404
|
}
|
|
405
|
+
/** Set custom widget type */
|
|
2923
406
|
widget(type) {
|
|
2924
407
|
this.control.ui = { ...this.control.ui, widget: type };
|
|
2925
408
|
return this;
|
|
2926
409
|
}
|
|
410
|
+
// ═══ I18N ═══
|
|
411
|
+
/** Add localized text for a locale */
|
|
2927
412
|
i18n(locale, translations) {
|
|
2928
413
|
this.control.i18n = { ...this.control.i18n, [locale]: translations };
|
|
2929
414
|
return this;
|
|
2930
415
|
}
|
|
416
|
+
// ═══ META ═══
|
|
417
|
+
/** Add custom metadata */
|
|
2931
418
|
meta(key, value) {
|
|
2932
419
|
this.control.meta = { ...this.control.meta, [key]: value };
|
|
2933
420
|
return this;
|
|
2934
421
|
}
|
|
422
|
+
// ═══ BUILD ═══
|
|
423
|
+
/**
|
|
424
|
+
* Build the final FormControl.
|
|
425
|
+
*
|
|
426
|
+
* Applies defaults and validates the control.
|
|
427
|
+
*
|
|
428
|
+
* @returns Complete FormControl object
|
|
429
|
+
*/
|
|
2935
430
|
build() {
|
|
2936
431
|
const control = {
|
|
2937
432
|
key: this.control.key,
|
|
@@ -2941,131 +436,190 @@ class ControlBuilder {
|
|
|
2941
436
|
};
|
|
2942
437
|
return control;
|
|
2943
438
|
}
|
|
2944
|
-
}
|
|
2945
|
-
|
|
2946
|
-
|
|
439
|
+
};
|
|
440
|
+
var FormBuilder = class _FormBuilder {
|
|
441
|
+
/** Partial form being built */
|
|
2947
442
|
form;
|
|
443
|
+
/**
|
|
444
|
+
* Create a new FormBuilder.
|
|
445
|
+
*
|
|
446
|
+
* @param id - Unique form identifier
|
|
447
|
+
*/
|
|
2948
448
|
constructor(id) {
|
|
2949
449
|
this.form = { id, controls: [] };
|
|
2950
450
|
}
|
|
451
|
+
// ═══ STATIC FACTORY ═══
|
|
452
|
+
/** Create a new form builder */
|
|
2951
453
|
static create(id) {
|
|
2952
|
-
return new
|
|
454
|
+
return new _FormBuilder(id);
|
|
2953
455
|
}
|
|
456
|
+
// ═══ METADATA ═══
|
|
457
|
+
/** Set form name */
|
|
2954
458
|
name(name) {
|
|
2955
459
|
this.form.name = name;
|
|
2956
460
|
return this;
|
|
2957
461
|
}
|
|
462
|
+
/** Set form description */
|
|
2958
463
|
description(desc) {
|
|
2959
464
|
this.form.description = desc;
|
|
2960
465
|
return this;
|
|
2961
466
|
}
|
|
467
|
+
/** Set form version */
|
|
2962
468
|
version(v) {
|
|
2963
469
|
this.form.version = v;
|
|
2964
470
|
return this;
|
|
2965
471
|
}
|
|
472
|
+
// ═══ CONTROLS ═══
|
|
473
|
+
/**
|
|
474
|
+
* Add a control to the form.
|
|
475
|
+
*
|
|
476
|
+
* Accepts either a ControlBuilder (calls .build()) or a FormControl.
|
|
477
|
+
*/
|
|
2966
478
|
control(builder) {
|
|
2967
479
|
const ctrl = builder instanceof ControlBuilder ? builder.build() : builder;
|
|
2968
480
|
this.form.controls?.push(ctrl);
|
|
2969
481
|
return this;
|
|
2970
482
|
}
|
|
483
|
+
/** Add multiple controls */
|
|
2971
484
|
controls(...builders) {
|
|
2972
485
|
for (const builder of builders) {
|
|
2973
486
|
this.control(builder);
|
|
2974
487
|
}
|
|
2975
488
|
return this;
|
|
2976
489
|
}
|
|
490
|
+
// ═══ SHORTHAND CONTROLS ═══
|
|
491
|
+
// WHY shorthands: Quick form prototyping
|
|
492
|
+
/** Add required text fields */
|
|
2977
493
|
required(...keys) {
|
|
2978
494
|
for (const key of keys) {
|
|
2979
495
|
this.control(ControlBuilder.field(key).required());
|
|
2980
496
|
}
|
|
2981
497
|
return this;
|
|
2982
498
|
}
|
|
499
|
+
/** Add optional text fields */
|
|
2983
500
|
optional(...keys) {
|
|
2984
501
|
for (const key of keys) {
|
|
2985
502
|
this.control(ControlBuilder.field(key));
|
|
2986
503
|
}
|
|
2987
504
|
return this;
|
|
2988
505
|
}
|
|
506
|
+
// ═══ PERMISSIONS ═══
|
|
507
|
+
/** Set roles that can start this form */
|
|
2989
508
|
roles(...roles) {
|
|
2990
509
|
this.form.roles = roles;
|
|
2991
510
|
return this;
|
|
2992
511
|
}
|
|
512
|
+
/** Allow multiple submissions per user */
|
|
2993
513
|
allowMultiple() {
|
|
2994
514
|
this.form.allowMultiple = true;
|
|
2995
515
|
return this;
|
|
2996
516
|
}
|
|
517
|
+
// ═══ UX ═══
|
|
518
|
+
/** Disable undo functionality */
|
|
2997
519
|
noUndo() {
|
|
2998
520
|
this.form.ux = { ...this.form.ux, allowUndo: false };
|
|
2999
521
|
return this;
|
|
3000
522
|
}
|
|
523
|
+
/** Disable skip functionality */
|
|
3001
524
|
noSkip() {
|
|
3002
525
|
this.form.ux = { ...this.form.ux, allowSkip: false };
|
|
3003
526
|
return this;
|
|
3004
527
|
}
|
|
528
|
+
/** Disable autofill */
|
|
3005
529
|
noAutofill() {
|
|
3006
530
|
this.form.ux = { ...this.form.ux, allowAutofill: false };
|
|
3007
531
|
return this;
|
|
3008
532
|
}
|
|
533
|
+
/** Set maximum undo steps */
|
|
3009
534
|
maxUndoSteps(n) {
|
|
3010
535
|
this.form.ux = { ...this.form.ux, maxUndoSteps: n };
|
|
3011
536
|
return this;
|
|
3012
537
|
}
|
|
538
|
+
// ═══ TTL ═══
|
|
539
|
+
/** Configure TTL (time-to-live) settings */
|
|
3013
540
|
ttl(config) {
|
|
3014
541
|
this.form.ttl = { ...this.form.ttl, ...config };
|
|
3015
542
|
return this;
|
|
3016
543
|
}
|
|
544
|
+
// ═══ NUDGE ═══
|
|
545
|
+
/** Disable nudge messages */
|
|
3017
546
|
noNudge() {
|
|
3018
547
|
this.form.nudge = { ...this.form.nudge, enabled: false };
|
|
3019
548
|
return this;
|
|
3020
549
|
}
|
|
550
|
+
/** Set inactivity hours before nudge */
|
|
3021
551
|
nudgeAfter(hours) {
|
|
3022
552
|
this.form.nudge = { ...this.form.nudge, afterInactiveHours: hours };
|
|
3023
553
|
return this;
|
|
3024
554
|
}
|
|
555
|
+
/** Set custom nudge message */
|
|
3025
556
|
nudgeMessage(message) {
|
|
3026
557
|
this.form.nudge = { ...this.form.nudge, message };
|
|
3027
558
|
return this;
|
|
3028
559
|
}
|
|
560
|
+
// ═══ HOOKS ═══
|
|
561
|
+
// WHY hooks: Allow consuming plugins to handle form events
|
|
562
|
+
/** Set task worker to call on session start */
|
|
3029
563
|
onStart(workerName) {
|
|
3030
564
|
this.form.hooks = { ...this.form.hooks, onStart: workerName };
|
|
3031
565
|
return this;
|
|
3032
566
|
}
|
|
567
|
+
/** Set task worker to call on field change */
|
|
3033
568
|
onFieldChange(workerName) {
|
|
3034
569
|
this.form.hooks = { ...this.form.hooks, onFieldChange: workerName };
|
|
3035
570
|
return this;
|
|
3036
571
|
}
|
|
572
|
+
/** Set task worker to call when form is ready to submit */
|
|
3037
573
|
onReady(workerName) {
|
|
3038
574
|
this.form.hooks = { ...this.form.hooks, onReady: workerName };
|
|
3039
575
|
return this;
|
|
3040
576
|
}
|
|
577
|
+
/** Set task worker to call on submission */
|
|
3041
578
|
onSubmit(workerName) {
|
|
3042
579
|
this.form.hooks = { ...this.form.hooks, onSubmit: workerName };
|
|
3043
580
|
return this;
|
|
3044
581
|
}
|
|
582
|
+
/** Set task worker to call on cancellation */
|
|
3045
583
|
onCancel(workerName) {
|
|
3046
584
|
this.form.hooks = { ...this.form.hooks, onCancel: workerName };
|
|
3047
585
|
return this;
|
|
3048
586
|
}
|
|
587
|
+
/** Set task worker to call on expiration */
|
|
3049
588
|
onExpire(workerName) {
|
|
3050
589
|
this.form.hooks = { ...this.form.hooks, onExpire: workerName };
|
|
3051
590
|
return this;
|
|
3052
591
|
}
|
|
592
|
+
/** Set multiple hooks at once */
|
|
3053
593
|
hooks(hooks) {
|
|
3054
594
|
this.form.hooks = { ...this.form.hooks, ...hooks };
|
|
3055
595
|
return this;
|
|
3056
596
|
}
|
|
597
|
+
// ═══ DEBUG ═══
|
|
598
|
+
/** Enable debug mode (logs extraction reasoning) */
|
|
3057
599
|
debug() {
|
|
3058
600
|
this.form.debug = true;
|
|
3059
601
|
return this;
|
|
3060
602
|
}
|
|
603
|
+
// ═══ I18N ═══
|
|
604
|
+
/** Add localized form text */
|
|
3061
605
|
i18n(locale, translations) {
|
|
3062
606
|
this.form.i18n = { ...this.form.i18n, [locale]: translations };
|
|
3063
607
|
return this;
|
|
3064
608
|
}
|
|
609
|
+
// ═══ META ═══
|
|
610
|
+
/** Add custom metadata */
|
|
3065
611
|
meta(key, value) {
|
|
3066
612
|
this.form.meta = { ...this.form.meta, [key]: value };
|
|
3067
613
|
return this;
|
|
3068
614
|
}
|
|
615
|
+
// ═══ BUILD ═══
|
|
616
|
+
/**
|
|
617
|
+
* Build the final FormDefinition.
|
|
618
|
+
*
|
|
619
|
+
* Applies defaults and validates the form.
|
|
620
|
+
*
|
|
621
|
+
* @returns Complete FormDefinition object
|
|
622
|
+
*/
|
|
3069
623
|
build() {
|
|
3070
624
|
const form = {
|
|
3071
625
|
id: this.form.id,
|
|
@@ -3075,132 +629,42 @@ class FormBuilder {
|
|
|
3075
629
|
};
|
|
3076
630
|
return form;
|
|
3077
631
|
}
|
|
3078
|
-
}
|
|
632
|
+
};
|
|
3079
633
|
var Form = FormBuilder;
|
|
3080
634
|
var C = ControlBuilder;
|
|
3081
635
|
function prettify2(key) {
|
|
3082
636
|
return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
3083
637
|
}
|
|
3084
638
|
|
|
3085
|
-
// index.ts
|
|
3086
|
-
init_service();
|
|
3087
|
-
init_restore();
|
|
3088
|
-
init_extractor();
|
|
3089
|
-
init_context();
|
|
3090
|
-
|
|
3091
|
-
// src/tasks/nudge.ts
|
|
3092
|
-
import { logger as logger5 } from "@elizaos/core";
|
|
3093
|
-
var formNudgeWorker = {
|
|
3094
|
-
name: "form_nudge_check",
|
|
3095
|
-
validate: async (_runtime, _message, _state) => {
|
|
3096
|
-
return true;
|
|
3097
|
-
},
|
|
3098
|
-
execute: async (runtime, _options, _task) => {
|
|
3099
|
-
try {
|
|
3100
|
-
const formService = runtime.getService("FORM");
|
|
3101
|
-
if (!formService) {
|
|
3102
|
-
logger5.debug("[FormNudge] Form service not available");
|
|
3103
|
-
return;
|
|
3104
|
-
}
|
|
3105
|
-
logger5.debug("[FormNudge] Nudge check cycle completed");
|
|
3106
|
-
} catch (error) {
|
|
3107
|
-
logger5.error("[FormNudge] Error during nudge check:", String(error));
|
|
3108
|
-
}
|
|
3109
|
-
}
|
|
3110
|
-
};
|
|
3111
|
-
async function processEntityNudges(runtime, entityId) {
|
|
3112
|
-
const formService = runtime.getService("FORM");
|
|
3113
|
-
if (!formService)
|
|
3114
|
-
return;
|
|
3115
|
-
const activeSessions = await formService.getAllActiveSessions(entityId);
|
|
3116
|
-
const stashedSessions = await formService.getStashedSessions(entityId);
|
|
3117
|
-
const allSessions = [...activeSessions, ...stashedSessions];
|
|
3118
|
-
const now = Date.now();
|
|
3119
|
-
const expirationWarningMs = 24 * 60 * 60 * 1000;
|
|
3120
|
-
for (const session of allSessions) {
|
|
3121
|
-
const form = formService.getForm(session.formId);
|
|
3122
|
-
if (session.expiresAt < now) {
|
|
3123
|
-
session.status = "expired";
|
|
3124
|
-
await formService.saveSession(session);
|
|
3125
|
-
if (form?.hooks?.onExpire) {
|
|
3126
|
-
const worker = runtime.getTaskWorker(form.hooks.onExpire);
|
|
3127
|
-
if (worker) {
|
|
3128
|
-
try {
|
|
3129
|
-
await worker.execute(runtime, { session, form }, {});
|
|
3130
|
-
} catch (error) {
|
|
3131
|
-
logger5.error("[FormNudge] onExpire hook failed:", String(error));
|
|
3132
|
-
}
|
|
3133
|
-
}
|
|
3134
|
-
}
|
|
3135
|
-
logger5.debug(`[FormNudge] Session ${session.id} expired`);
|
|
3136
|
-
continue;
|
|
3137
|
-
}
|
|
3138
|
-
if (isExpiringSoon(session, expirationWarningMs) && !session.expirationWarned) {
|
|
3139
|
-
await sendExpirationWarning(runtime, session, form);
|
|
3140
|
-
session.expirationWarned = true;
|
|
3141
|
-
await formService.saveSession(session);
|
|
3142
|
-
continue;
|
|
3143
|
-
}
|
|
3144
|
-
if (session.status === "stashed" && shouldNudge(session, form)) {
|
|
3145
|
-
await sendNudge(runtime, session, form);
|
|
3146
|
-
session.nudgeCount = (session.nudgeCount || 0) + 1;
|
|
3147
|
-
session.lastNudgeAt = now;
|
|
3148
|
-
await formService.saveSession(session);
|
|
3149
|
-
}
|
|
3150
|
-
}
|
|
3151
|
-
}
|
|
3152
|
-
async function sendNudge(runtime, session, form) {
|
|
3153
|
-
const message = form?.nudge?.message || `You have an unfinished "${form?.name || "form"}". Would you like to continue?`;
|
|
3154
|
-
try {
|
|
3155
|
-
if (typeof runtime.sendMessageToRoom === "function") {
|
|
3156
|
-
await runtime.sendMessageToRoom(session.roomId, {
|
|
3157
|
-
text: message
|
|
3158
|
-
});
|
|
3159
|
-
logger5.debug(`[FormNudge] Sent nudge for session in room ${session.roomId}`);
|
|
3160
|
-
}
|
|
3161
|
-
} catch (error) {
|
|
3162
|
-
logger5.error("[FormNudge] Failed to send nudge:", String(error));
|
|
3163
|
-
}
|
|
3164
|
-
}
|
|
3165
|
-
async function sendExpirationWarning(runtime, session, form) {
|
|
3166
|
-
const remaining = formatTimeRemaining(session);
|
|
3167
|
-
const message = `Your "${form?.name || "form"}" form will expire in ${remaining}. Say "resume" to keep working on it.`;
|
|
3168
|
-
try {
|
|
3169
|
-
if (typeof runtime.sendMessageToRoom === "function") {
|
|
3170
|
-
await runtime.sendMessageToRoom(session.roomId, {
|
|
3171
|
-
text: message
|
|
3172
|
-
});
|
|
3173
|
-
logger5.debug(`[FormNudge] Sent expiration warning for session in room ${session.roomId}`);
|
|
3174
|
-
}
|
|
3175
|
-
} catch (error) {
|
|
3176
|
-
logger5.error("[FormNudge] Failed to send expiration warning:", String(error));
|
|
3177
|
-
}
|
|
3178
|
-
}
|
|
3179
|
-
|
|
3180
|
-
// index.ts
|
|
639
|
+
// src/index.ts
|
|
3181
640
|
var formPlugin = {
|
|
3182
641
|
name: "form",
|
|
3183
642
|
description: "Agent-native conversational forms for data collection",
|
|
643
|
+
// Service for form management
|
|
3184
644
|
services: [
|
|
645
|
+
// FormService is registered as a static class
|
|
646
|
+
// It will be instantiated by the runtime
|
|
3185
647
|
{
|
|
3186
648
|
serviceType: "FORM",
|
|
3187
649
|
start: async (runtime) => {
|
|
3188
|
-
const { FormService: FormService2 } = await
|
|
650
|
+
const { FormService: FormService2 } = await import("./service-TCCXKV3T.js");
|
|
3189
651
|
return FormService2.start(runtime);
|
|
3190
652
|
}
|
|
3191
653
|
}
|
|
3192
654
|
],
|
|
655
|
+
// Provider for form context
|
|
3193
656
|
providers: [
|
|
657
|
+
// Import dynamically to avoid circular deps
|
|
3194
658
|
{
|
|
3195
659
|
name: "FORM_CONTEXT",
|
|
3196
660
|
description: "Provides context about active form sessions",
|
|
3197
|
-
dynamic: true,
|
|
3198
661
|
get: async (runtime, message, state) => {
|
|
3199
|
-
const { formContextProvider: formContextProvider2 } = await
|
|
662
|
+
const { formContextProvider: formContextProvider2 } = await import("./context-MHPFYZZ2.js");
|
|
3200
663
|
return formContextProvider2.get(runtime, message, state);
|
|
3201
664
|
}
|
|
3202
665
|
}
|
|
3203
666
|
],
|
|
667
|
+
// Evaluator for field extraction
|
|
3204
668
|
evaluators: [
|
|
3205
669
|
{
|
|
3206
670
|
name: "form_evaluator",
|
|
@@ -3208,26 +672,27 @@ var formPlugin = {
|
|
|
3208
672
|
similes: ["FORM_EXTRACTION", "FORM_HANDLER"],
|
|
3209
673
|
examples: [],
|
|
3210
674
|
validate: async (runtime, message, state) => {
|
|
3211
|
-
const { formEvaluator: formEvaluator2 } = await
|
|
675
|
+
const { formEvaluator: formEvaluator2 } = await import("./extractor-UWASKXKD.js");
|
|
3212
676
|
return formEvaluator2.validate(runtime, message, state);
|
|
3213
677
|
},
|
|
3214
678
|
handler: async (runtime, message, state) => {
|
|
3215
|
-
const { formEvaluator: formEvaluator2 } = await
|
|
679
|
+
const { formEvaluator: formEvaluator2 } = await import("./extractor-UWASKXKD.js");
|
|
3216
680
|
return formEvaluator2.handler(runtime, message, state);
|
|
3217
681
|
}
|
|
3218
682
|
}
|
|
3219
683
|
],
|
|
684
|
+
// Action for restoring stashed forms
|
|
3220
685
|
actions: [
|
|
3221
686
|
{
|
|
3222
687
|
name: "FORM_RESTORE",
|
|
3223
688
|
similes: ["RESUME_FORM", "CONTINUE_FORM"],
|
|
3224
689
|
description: "Restore a previously stashed form session",
|
|
3225
690
|
validate: async (runtime, message, state) => {
|
|
3226
|
-
const { formRestoreAction: formRestoreAction2 } = await
|
|
691
|
+
const { formRestoreAction: formRestoreAction2 } = await import("./restore-S7JLME4H.js");
|
|
3227
692
|
return formRestoreAction2.validate(runtime, message, state);
|
|
3228
693
|
},
|
|
3229
694
|
handler: async (runtime, message, state, options, callback) => {
|
|
3230
|
-
const { formRestoreAction: formRestoreAction2 } = await
|
|
695
|
+
const { formRestoreAction: formRestoreAction2 } = await import("./restore-S7JLME4H.js");
|
|
3231
696
|
return formRestoreAction2.handler(runtime, message, state, options, callback);
|
|
3232
697
|
},
|
|
3233
698
|
examples: [
|
|
@@ -3247,63 +712,197 @@ var formPlugin = {
|
|
|
3247
712
|
}
|
|
3248
713
|
]
|
|
3249
714
|
};
|
|
3250
|
-
var
|
|
715
|
+
var index_default = formPlugin;
|
|
3251
716
|
export {
|
|
3252
|
-
|
|
3253
|
-
shouldNudge,
|
|
3254
|
-
shouldConfirmCancel,
|
|
3255
|
-
saveSubmission,
|
|
3256
|
-
saveSession,
|
|
3257
|
-
saveAutofillData,
|
|
3258
|
-
registerTypeHandler,
|
|
3259
|
-
registerBuiltinTypes,
|
|
3260
|
-
quickIntentDetect,
|
|
3261
|
-
processEntityNudges,
|
|
3262
|
-
prettify,
|
|
3263
|
-
parseValue,
|
|
3264
|
-
matchesMimeType,
|
|
3265
|
-
llmIntentAndExtract,
|
|
3266
|
-
isUXIntent,
|
|
3267
|
-
isLifecycleIntent,
|
|
3268
|
-
isExpiringSoon,
|
|
3269
|
-
isExpired,
|
|
3270
|
-
isBuiltinType,
|
|
3271
|
-
hasDataToExtract,
|
|
3272
|
-
getTypeHandler,
|
|
3273
|
-
getSubmissions,
|
|
3274
|
-
getStashedSessions,
|
|
3275
|
-
getBuiltinType,
|
|
3276
|
-
getAutofillData,
|
|
3277
|
-
getAllActiveSessions,
|
|
3278
|
-
getActiveSession,
|
|
3279
|
-
formatValue,
|
|
3280
|
-
formatTimeRemaining,
|
|
3281
|
-
formatEffort,
|
|
3282
|
-
formRestoreAction,
|
|
3283
|
-
formPlugin,
|
|
3284
|
-
formNudgeWorker,
|
|
3285
|
-
formEvaluator,
|
|
3286
|
-
formContextProvider,
|
|
3287
|
-
extractSingleField,
|
|
3288
|
-
detectCorrection,
|
|
3289
|
-
deleteSession,
|
|
3290
|
-
typescript_default as default,
|
|
3291
|
-
clearTypeHandlers,
|
|
3292
|
-
calculateTTL,
|
|
3293
|
-
applyFormDefaults,
|
|
3294
|
-
applyControlDefaults,
|
|
3295
|
-
FormService,
|
|
3296
|
-
FormBuilder,
|
|
3297
|
-
Form,
|
|
3298
|
-
FORM_SUBMISSION_COMPONENT,
|
|
3299
|
-
FORM_SESSION_COMPONENT,
|
|
3300
|
-
FORM_DEFINITION_DEFAULTS,
|
|
3301
|
-
FORM_CONTROL_DEFAULTS,
|
|
3302
|
-
FORM_AUTOFILL_COMPONENT,
|
|
3303
|
-
ControlBuilder,
|
|
3304
|
-
C,
|
|
717
|
+
BUILTIN_TYPES,
|
|
3305
718
|
BUILTIN_TYPE_MAP,
|
|
3306
|
-
|
|
719
|
+
C,
|
|
720
|
+
ControlBuilder,
|
|
721
|
+
FORM_AUTOFILL_COMPONENT,
|
|
722
|
+
FORM_CONTROL_DEFAULTS,
|
|
723
|
+
FORM_DEFINITION_DEFAULTS,
|
|
724
|
+
FORM_SESSION_COMPONENT,
|
|
725
|
+
FORM_SUBMISSION_COMPONENT,
|
|
726
|
+
Form,
|
|
727
|
+
FormBuilder,
|
|
728
|
+
FormService,
|
|
729
|
+
applyControlDefaults,
|
|
730
|
+
applyFormDefaults,
|
|
731
|
+
calculateTTL,
|
|
732
|
+
clearTypeHandlers,
|
|
733
|
+
index_default as default,
|
|
734
|
+
deleteSession,
|
|
735
|
+
detectCorrection,
|
|
736
|
+
extractSingleField,
|
|
737
|
+
formContextProvider,
|
|
738
|
+
formEvaluator,
|
|
739
|
+
formPlugin,
|
|
740
|
+
formRestoreAction,
|
|
741
|
+
formatEffort,
|
|
742
|
+
formatTimeRemaining,
|
|
743
|
+
formatValue,
|
|
744
|
+
getActiveSession,
|
|
745
|
+
getAllActiveSessions,
|
|
746
|
+
getAutofillData,
|
|
747
|
+
getBuiltinType,
|
|
748
|
+
getStashedSessions,
|
|
749
|
+
getSubmissions,
|
|
750
|
+
getTypeHandler,
|
|
751
|
+
hasDataToExtract,
|
|
752
|
+
isBuiltinType,
|
|
753
|
+
isExpired,
|
|
754
|
+
isExpiringSoon,
|
|
755
|
+
isLifecycleIntent,
|
|
756
|
+
isUXIntent,
|
|
757
|
+
llmIntentAndExtract,
|
|
758
|
+
matchesMimeType,
|
|
759
|
+
parseValue,
|
|
760
|
+
prettify,
|
|
761
|
+
quickIntentDetect,
|
|
762
|
+
registerBuiltinTypes,
|
|
763
|
+
registerTypeHandler,
|
|
764
|
+
saveAutofillData,
|
|
765
|
+
saveSession,
|
|
766
|
+
saveSubmission,
|
|
767
|
+
shouldConfirmCancel,
|
|
768
|
+
shouldNudge,
|
|
769
|
+
validateField
|
|
3307
770
|
};
|
|
3308
|
-
|
|
3309
|
-
|
|
771
|
+
/**
|
|
772
|
+
* @module @elizaos/plugin-form
|
|
773
|
+
* @description Guardrails for agent-guided user journeys
|
|
774
|
+
*
|
|
775
|
+
* @author Odilitime
|
|
776
|
+
* @copyright 2025 Odilitime
|
|
777
|
+
* @license MIT
|
|
778
|
+
*
|
|
779
|
+
* ## The Core Insight
|
|
780
|
+
*
|
|
781
|
+
* Forms aren't just about data collection - they're **guardrails for agents**.
|
|
782
|
+
*
|
|
783
|
+
* Without structure, agents wander. They forget context, miss required
|
|
784
|
+
* information, and can't reliably guide users to outcomes. This plugin
|
|
785
|
+
* gives agents the tools to follow conventions and shepherd users through
|
|
786
|
+
* structured journeys - registrations, orders, applications, onboarding flows.
|
|
787
|
+
*
|
|
788
|
+
* **Forms define the path. Agents follow it. Users reach outcomes.**
|
|
789
|
+
*
|
|
790
|
+
* ## Key Features
|
|
791
|
+
*
|
|
792
|
+
* - **Natural Language Extraction**: "I'm John, 25, john@example.com"
|
|
793
|
+
* - **Two-Tier Intent Detection**: Fast English keywords + LLM fallback
|
|
794
|
+
* - **UX Magic**: Undo, skip, explain, example, progress, autofill
|
|
795
|
+
* - **Smart TTL**: Retention scales with user effort
|
|
796
|
+
* - **Fluent Builder API**: Type-safe form definitions
|
|
797
|
+
* - **Extensible Types**: Register custom field types
|
|
798
|
+
*
|
|
799
|
+
* ## Architecture
|
|
800
|
+
*
|
|
801
|
+
* ```
|
|
802
|
+
* ┌─────────────────────────────────────────────────────────────┐
|
|
803
|
+
* │ Form Plugin │
|
|
804
|
+
* ├─────────────────────────────────────────────────────────────┤
|
|
805
|
+
* │ │
|
|
806
|
+
* │ Provider (FORM_CONTEXT) │
|
|
807
|
+
* │ - Runs BEFORE agent responds │
|
|
808
|
+
* │ - Injects form state into context │
|
|
809
|
+
* │ - Tells agent what to ask next │
|
|
810
|
+
* │ │
|
|
811
|
+
* │ Evaluator (form_evaluator) │
|
|
812
|
+
* │ - Runs AFTER each user message │
|
|
813
|
+
* │ - Detects intent (submit, cancel, undo, etc.) │
|
|
814
|
+
* │ - Extracts field values from natural language │
|
|
815
|
+
* │ - Updates session state │
|
|
816
|
+
* │ │
|
|
817
|
+
* │ Action (FORM_RESTORE) │
|
|
818
|
+
* │ - Preempts REPLY for restore intent │
|
|
819
|
+
* │ - Immediately restores stashed forms │
|
|
820
|
+
* │ │
|
|
821
|
+
* │ Service (FormService) │
|
|
822
|
+
* │ - Manages form definitions │
|
|
823
|
+
* │ - Manages sessions, submissions, autofill │
|
|
824
|
+
* │ - Executes lifecycle hooks │
|
|
825
|
+
* │ │
|
|
826
|
+
* └─────────────────────────────────────────────────────────────┘
|
|
827
|
+
* ```
|
|
828
|
+
*
|
|
829
|
+
* ## Quick Start
|
|
830
|
+
*
|
|
831
|
+
* ### 1. Add plugin to your agent
|
|
832
|
+
*
|
|
833
|
+
* ```typescript
|
|
834
|
+
* import { formPlugin } from '@elizaos/plugin-form';
|
|
835
|
+
*
|
|
836
|
+
* const agent = {
|
|
837
|
+
* plugins: [formPlugin, ...otherPlugins],
|
|
838
|
+
* };
|
|
839
|
+
* ```
|
|
840
|
+
*
|
|
841
|
+
* ### 2. Define a form
|
|
842
|
+
*
|
|
843
|
+
* ```typescript
|
|
844
|
+
* import { Form, C } from '@elizaos/plugin-form';
|
|
845
|
+
*
|
|
846
|
+
* const registrationForm = Form.create('registration')
|
|
847
|
+
* .name('User Registration')
|
|
848
|
+
* .control(C.email('email').required().ask('What email should we use?'))
|
|
849
|
+
* .control(C.text('name').required().ask("What's your name?"))
|
|
850
|
+
* .control(C.number('age').min(13))
|
|
851
|
+
* .onSubmit('handle_registration')
|
|
852
|
+
* .build();
|
|
853
|
+
* ```
|
|
854
|
+
*
|
|
855
|
+
* ### 3. Register and start
|
|
856
|
+
*
|
|
857
|
+
* ```typescript
|
|
858
|
+
* // In your plugin init:
|
|
859
|
+
* const formService = runtime.getService('FORM') as FormService;
|
|
860
|
+
* formService.registerForm(registrationForm);
|
|
861
|
+
*
|
|
862
|
+
* // When you need to collect data:
|
|
863
|
+
* await formService.startSession('registration', entityId, roomId);
|
|
864
|
+
* ```
|
|
865
|
+
*
|
|
866
|
+
* ### 4. Handle submissions
|
|
867
|
+
*
|
|
868
|
+
* ```typescript
|
|
869
|
+
* runtime.registerTaskWorker({
|
|
870
|
+
* name: 'handle_registration',
|
|
871
|
+
* execute: async (runtime, options) => {
|
|
872
|
+
* const { submission } = options;
|
|
873
|
+
* const { email, name, age } = submission.values;
|
|
874
|
+
* // Create user account, etc.
|
|
875
|
+
* }
|
|
876
|
+
* });
|
|
877
|
+
* ```
|
|
878
|
+
*
|
|
879
|
+
* ## User Experience
|
|
880
|
+
*
|
|
881
|
+
* The form plugin handles these user interactions:
|
|
882
|
+
*
|
|
883
|
+
* | User Says | Intent | Result |
|
|
884
|
+
* |-----------|--------|--------|
|
|
885
|
+
* | "I'm John, 25 years old" | fill_form | Extract name=John, age=25 |
|
|
886
|
+
* | "done" / "submit" | submit | Submit the form |
|
|
887
|
+
* | "save for later" | stash | Save and switch contexts |
|
|
888
|
+
* | "resume my form" | restore | Restore stashed form |
|
|
889
|
+
* | "cancel" / "nevermind" | cancel | Abandon form |
|
|
890
|
+
* | "undo" / "go back" | undo | Revert last change |
|
|
891
|
+
* | "skip" | skip | Skip optional field |
|
|
892
|
+
* | "why?" | explain | Explain current field |
|
|
893
|
+
* | "example?" | example | Show example value |
|
|
894
|
+
* | "how far?" | progress | Show completion status |
|
|
895
|
+
* | "same as last time" | autofill | Use saved values |
|
|
896
|
+
*
|
|
897
|
+
* ## Module Exports
|
|
898
|
+
*
|
|
899
|
+
* - **Types**: FormControl, FormDefinition, FormSession, etc.
|
|
900
|
+
* - **Builder**: Form, C (ControlBuilder)
|
|
901
|
+
* - **Service**: FormService
|
|
902
|
+
* - **Utilities**: validateField, parseValue, formatValue
|
|
903
|
+
* - **Plugin**: formPlugin (default export)
|
|
904
|
+
*
|
|
905
|
+
* @see {@link FormService} for form management API
|
|
906
|
+
* @see {@link FormBuilder} for fluent form definition
|
|
907
|
+
* @see {@link ControlBuilder} for field definition
|
|
908
|
+
*/
|