@ai-gui/plugin-form 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +33 -5
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +33 -5
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -51,7 +51,8 @@ const FIELD_KEYS = new Set([
|
|
|
51
51
|
"min",
|
|
52
52
|
"max",
|
|
53
53
|
"options",
|
|
54
|
-
"placeholder"
|
|
54
|
+
"placeholder",
|
|
55
|
+
"expect"
|
|
55
56
|
]);
|
|
56
57
|
const OPTION_KEYS = new Set(["label", "value"]);
|
|
57
58
|
const SAFE_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
@@ -232,12 +233,16 @@ function parseField(value, index, issues) {
|
|
|
232
233
|
issues.push(`${path}.pattern must be a valid regular expression.`);
|
|
233
234
|
}
|
|
234
235
|
}
|
|
236
|
+
let expected;
|
|
237
|
+
if (value.expect !== void 0) if (typeof value.expect === "string" || typeof value.expect === "number" || typeof value.expect === "boolean") expected = value.expect;
|
|
238
|
+
else issues.push(`${path}.expect must be a string, a number, or a boolean.`);
|
|
235
239
|
if (!name || !label || !type) return void 0;
|
|
236
240
|
const base = {
|
|
237
241
|
name,
|
|
238
242
|
label,
|
|
239
243
|
...value.required === true ? { required: true } : {},
|
|
240
|
-
...placeholder === void 0 ? {} : { placeholder }
|
|
244
|
+
...placeholder === void 0 ? {} : { placeholder },
|
|
245
|
+
...expected === void 0 ? {} : { expect: expected }
|
|
241
246
|
};
|
|
242
247
|
if (type === "number") {
|
|
243
248
|
if (minLength !== void 0 || maxLength !== void 0 || pattern !== void 0 || value.options !== void 0) issues.push(`${path} contains constraints unsupported by number fields.`);
|
|
@@ -342,13 +347,35 @@ function mountForm(host, definition, options) {
|
|
|
342
347
|
submit.textContent = definition.submitLabel ?? "Submit";
|
|
343
348
|
formElement.appendChild(submit);
|
|
344
349
|
/**
|
|
350
|
+
* Compare the submission with the answers the fields declared.
|
|
351
|
+
*
|
|
352
|
+
* Only fields carrying `expect` take part, so a plain form is never marked. The tone is the worst
|
|
353
|
+
* of them: one wrong answer among three right ones is not a pass.
|
|
354
|
+
*/
|
|
355
|
+
const gradeAgainstExpectations = (values) => {
|
|
356
|
+
const graded = definition.fields.filter((field) => field.expect !== void 0);
|
|
357
|
+
if (graded.length === 0) return void 0;
|
|
358
|
+
const fieldTones = {};
|
|
359
|
+
let wrong = 0;
|
|
360
|
+
for (const field of graded) {
|
|
361
|
+
const submitted$1 = values[field.name];
|
|
362
|
+
const matches = typeof field.expect === "string" && typeof submitted$1 === "string" ? submitted$1.trim() === field.expect.trim() : submitted$1 === field.expect;
|
|
363
|
+
fieldTones[field.name] = matches ? "positive" : "warning";
|
|
364
|
+
if (!matches) wrong += 1;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
tone: wrong === 0 ? "positive" : "warning",
|
|
368
|
+
fields: fieldTones
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
/**
|
|
345
372
|
* Show how the submission turned out, when the handler judged it.
|
|
346
373
|
*
|
|
347
374
|
* The result used to be discarded, so an app that knew the answer was wrong had no way to say so
|
|
348
375
|
* — the form simply disabled itself and read "Submitted" whether the answer was right or not.
|
|
349
376
|
*/
|
|
350
|
-
const applyOutcome = (result) => {
|
|
351
|
-
const outcome = (0, __ai_gui_core.actionOutcome)(result);
|
|
377
|
+
const applyOutcome = (result, fallback) => {
|
|
378
|
+
const outcome = (0, __ai_gui_core.actionOutcome)(result) ?? fallback;
|
|
352
379
|
if (!outcome || disposed) return;
|
|
353
380
|
formElement.setAttribute("data-aigui-form-outcome", outcome.tone);
|
|
354
381
|
if (outcome.message) {
|
|
@@ -385,6 +412,7 @@ function mountForm(host, definition, options) {
|
|
|
385
412
|
pending = true;
|
|
386
413
|
submit.disabled = true;
|
|
387
414
|
formElement.setAttribute("aria-busy", "true");
|
|
415
|
+
const graded = gradeAgainstExpectations(validation.values);
|
|
388
416
|
const settle = () => {
|
|
389
417
|
if (disposed) return;
|
|
390
418
|
pending = false;
|
|
@@ -400,7 +428,7 @@ function mountForm(host, definition, options) {
|
|
|
400
428
|
signal: controller.signal
|
|
401
429
|
}).then((result) => {
|
|
402
430
|
markSubmitted();
|
|
403
|
-
applyOutcome(result);
|
|
431
|
+
applyOutcome(result, graded);
|
|
404
432
|
}, (error) => {
|
|
405
433
|
if (!disposed && !controller.signal.aborted) {
|
|
406
434
|
actionError.textContent = actionErrorMessage(error);
|
package/dist/index.d.cts
CHANGED
|
@@ -12,6 +12,15 @@ interface FormFieldBase {
|
|
|
12
12
|
label: string;
|
|
13
13
|
required?: boolean;
|
|
14
14
|
placeholder?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The answer this field is expected to carry.
|
|
17
|
+
*
|
|
18
|
+
* Declaring it lets the form report how the submission compared — a quiz colours itself the
|
|
19
|
+
* moment it is answered instead of waiting for a round trip to say so. Unlike the constraints
|
|
20
|
+
* beside it this never blocks a submission: a wrong answer is an answer, and the person is told
|
|
21
|
+
* rather than stopped.
|
|
22
|
+
*/
|
|
23
|
+
expect?: string | number | boolean;
|
|
15
24
|
}
|
|
16
25
|
interface FormStringField extends FormFieldBase {
|
|
17
26
|
type: "text" | "textarea";
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ interface FormFieldBase {
|
|
|
12
12
|
label: string;
|
|
13
13
|
required?: boolean;
|
|
14
14
|
placeholder?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The answer this field is expected to carry.
|
|
17
|
+
*
|
|
18
|
+
* Declaring it lets the form report how the submission compared — a quiz colours itself the
|
|
19
|
+
* moment it is answered instead of waiting for a round trip to say so. Unlike the constraints
|
|
20
|
+
* beside it this never blocks a submission: a wrong answer is an answer, and the person is told
|
|
21
|
+
* rather than stopped.
|
|
22
|
+
*/
|
|
23
|
+
expect?: string | number | boolean;
|
|
15
24
|
}
|
|
16
25
|
interface FormStringField extends FormFieldBase {
|
|
17
26
|
type: "text" | "textarea";
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,8 @@ const FIELD_KEYS = new Set([
|
|
|
27
27
|
"min",
|
|
28
28
|
"max",
|
|
29
29
|
"options",
|
|
30
|
-
"placeholder"
|
|
30
|
+
"placeholder",
|
|
31
|
+
"expect"
|
|
31
32
|
]);
|
|
32
33
|
const OPTION_KEYS = new Set(["label", "value"]);
|
|
33
34
|
const SAFE_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
@@ -208,12 +209,16 @@ function parseField(value, index, issues) {
|
|
|
208
209
|
issues.push(`${path}.pattern must be a valid regular expression.`);
|
|
209
210
|
}
|
|
210
211
|
}
|
|
212
|
+
let expected;
|
|
213
|
+
if (value.expect !== void 0) if (typeof value.expect === "string" || typeof value.expect === "number" || typeof value.expect === "boolean") expected = value.expect;
|
|
214
|
+
else issues.push(`${path}.expect must be a string, a number, or a boolean.`);
|
|
211
215
|
if (!name || !label || !type) return void 0;
|
|
212
216
|
const base = {
|
|
213
217
|
name,
|
|
214
218
|
label,
|
|
215
219
|
...value.required === true ? { required: true } : {},
|
|
216
|
-
...placeholder === void 0 ? {} : { placeholder }
|
|
220
|
+
...placeholder === void 0 ? {} : { placeholder },
|
|
221
|
+
...expected === void 0 ? {} : { expect: expected }
|
|
217
222
|
};
|
|
218
223
|
if (type === "number") {
|
|
219
224
|
if (minLength !== void 0 || maxLength !== void 0 || pattern !== void 0 || value.options !== void 0) issues.push(`${path} contains constraints unsupported by number fields.`);
|
|
@@ -318,13 +323,35 @@ function mountForm(host, definition, options) {
|
|
|
318
323
|
submit.textContent = definition.submitLabel ?? "Submit";
|
|
319
324
|
formElement.appendChild(submit);
|
|
320
325
|
/**
|
|
326
|
+
* Compare the submission with the answers the fields declared.
|
|
327
|
+
*
|
|
328
|
+
* Only fields carrying `expect` take part, so a plain form is never marked. The tone is the worst
|
|
329
|
+
* of them: one wrong answer among three right ones is not a pass.
|
|
330
|
+
*/
|
|
331
|
+
const gradeAgainstExpectations = (values) => {
|
|
332
|
+
const graded = definition.fields.filter((field) => field.expect !== void 0);
|
|
333
|
+
if (graded.length === 0) return void 0;
|
|
334
|
+
const fieldTones = {};
|
|
335
|
+
let wrong = 0;
|
|
336
|
+
for (const field of graded) {
|
|
337
|
+
const submitted$1 = values[field.name];
|
|
338
|
+
const matches = typeof field.expect === "string" && typeof submitted$1 === "string" ? submitted$1.trim() === field.expect.trim() : submitted$1 === field.expect;
|
|
339
|
+
fieldTones[field.name] = matches ? "positive" : "warning";
|
|
340
|
+
if (!matches) wrong += 1;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
tone: wrong === 0 ? "positive" : "warning",
|
|
344
|
+
fields: fieldTones
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
321
348
|
* Show how the submission turned out, when the handler judged it.
|
|
322
349
|
*
|
|
323
350
|
* The result used to be discarded, so an app that knew the answer was wrong had no way to say so
|
|
324
351
|
* — the form simply disabled itself and read "Submitted" whether the answer was right or not.
|
|
325
352
|
*/
|
|
326
|
-
const applyOutcome = (result) => {
|
|
327
|
-
const outcome = actionOutcome(result);
|
|
353
|
+
const applyOutcome = (result, fallback) => {
|
|
354
|
+
const outcome = actionOutcome(result) ?? fallback;
|
|
328
355
|
if (!outcome || disposed) return;
|
|
329
356
|
formElement.setAttribute("data-aigui-form-outcome", outcome.tone);
|
|
330
357
|
if (outcome.message) {
|
|
@@ -361,6 +388,7 @@ function mountForm(host, definition, options) {
|
|
|
361
388
|
pending = true;
|
|
362
389
|
submit.disabled = true;
|
|
363
390
|
formElement.setAttribute("aria-busy", "true");
|
|
391
|
+
const graded = gradeAgainstExpectations(validation.values);
|
|
364
392
|
const settle = () => {
|
|
365
393
|
if (disposed) return;
|
|
366
394
|
pending = false;
|
|
@@ -376,7 +404,7 @@ function mountForm(host, definition, options) {
|
|
|
376
404
|
signal: controller.signal
|
|
377
405
|
}).then((result) => {
|
|
378
406
|
markSubmitted();
|
|
379
|
-
applyOutcome(result);
|
|
407
|
+
applyOutcome(result, graded);
|
|
380
408
|
}, (error) => {
|
|
381
409
|
if (!disposed && !controller.signal.aborted) {
|
|
382
410
|
actionError.textContent = actionErrorMessage(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-gui/plugin-form",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Safe interactive form blocks for AIGUI, backed by the core ActionRuntime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ai-gui/core": "0.
|
|
52
|
+
"@ai-gui/core": "0.8.0"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsdown",
|