@ai-gui/plugin-form 0.6.2 → 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 +57 -3
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +58 -4
- 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.`);
|
|
@@ -318,22 +323,67 @@ function mountForm(host, definition, options) {
|
|
|
318
323
|
formElement.setAttribute("data-aigui-form-instance", instanceId);
|
|
319
324
|
const controls = new Map();
|
|
320
325
|
const errorElements = new Map();
|
|
326
|
+
const fieldContainers = new Map();
|
|
321
327
|
for (const field of definition.fields) {
|
|
322
328
|
const rendered = createField(instancePrefix, field);
|
|
323
329
|
formElement.appendChild(rendered.container);
|
|
324
330
|
controls.set(field.name, rendered.control);
|
|
325
331
|
errorElements.set(field.name, rendered.error);
|
|
332
|
+
fieldContainers.set(field.name, rendered.container);
|
|
326
333
|
}
|
|
327
334
|
const actionError = document.createElement("div");
|
|
328
335
|
actionError.setAttribute("data-aigui-form-action-error", "");
|
|
329
336
|
actionError.setAttribute("role", "alert");
|
|
330
337
|
actionError.hidden = true;
|
|
331
338
|
formElement.appendChild(actionError);
|
|
339
|
+
const outcomeMessage = document.createElement("div");
|
|
340
|
+
outcomeMessage.setAttribute("data-aigui-form-outcome-message", "");
|
|
341
|
+
outcomeMessage.setAttribute("aria-live", "polite");
|
|
342
|
+
outcomeMessage.hidden = true;
|
|
343
|
+
formElement.appendChild(outcomeMessage);
|
|
332
344
|
const submit = document.createElement("button");
|
|
333
345
|
submit.type = "button";
|
|
334
346
|
submit.setAttribute("data-aigui-form-submit", "");
|
|
335
347
|
submit.textContent = definition.submitLabel ?? "Submit";
|
|
336
348
|
formElement.appendChild(submit);
|
|
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
|
+
/**
|
|
372
|
+
* Show how the submission turned out, when the handler judged it.
|
|
373
|
+
*
|
|
374
|
+
* The result used to be discarded, so an app that knew the answer was wrong had no way to say so
|
|
375
|
+
* — the form simply disabled itself and read "Submitted" whether the answer was right or not.
|
|
376
|
+
*/
|
|
377
|
+
const applyOutcome = (result, fallback) => {
|
|
378
|
+
const outcome = (0, __ai_gui_core.actionOutcome)(result) ?? fallback;
|
|
379
|
+
if (!outcome || disposed) return;
|
|
380
|
+
formElement.setAttribute("data-aigui-form-outcome", outcome.tone);
|
|
381
|
+
if (outcome.message) {
|
|
382
|
+
outcomeMessage.textContent = outcome.message;
|
|
383
|
+
outcomeMessage.hidden = false;
|
|
384
|
+
}
|
|
385
|
+
for (const [name, tone] of Object.entries(outcome.fields ?? {})) fieldContainers.get(name)?.setAttribute("data-aigui-form-field-outcome", tone);
|
|
386
|
+
};
|
|
337
387
|
const markSubmitted = () => {
|
|
338
388
|
if (disposed) return;
|
|
339
389
|
pending = false;
|
|
@@ -362,6 +412,7 @@ function mountForm(host, definition, options) {
|
|
|
362
412
|
pending = true;
|
|
363
413
|
submit.disabled = true;
|
|
364
414
|
formElement.setAttribute("aria-busy", "true");
|
|
415
|
+
const graded = gradeAgainstExpectations(validation.values);
|
|
365
416
|
const settle = () => {
|
|
366
417
|
if (disposed) return;
|
|
367
418
|
pending = false;
|
|
@@ -375,7 +426,10 @@ function mountForm(host, definition, options) {
|
|
|
375
426
|
}, {
|
|
376
427
|
owner,
|
|
377
428
|
signal: controller.signal
|
|
378
|
-
}).then(
|
|
429
|
+
}).then((result) => {
|
|
430
|
+
markSubmitted();
|
|
431
|
+
applyOutcome(result, graded);
|
|
432
|
+
}, (error) => {
|
|
379
433
|
if (!disposed && !controller.signal.aborted) {
|
|
380
434
|
actionError.textContent = actionErrorMessage(error);
|
|
381
435
|
actionError.hidden = false;
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionRuntimeError } from "@ai-gui/core";
|
|
1
|
+
import { ActionRuntimeError, actionOutcome } from "@ai-gui/core";
|
|
2
2
|
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const FIELD_TYPES = new Set([
|
|
@@ -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.`);
|
|
@@ -294,22 +299,67 @@ function mountForm(host, definition, options) {
|
|
|
294
299
|
formElement.setAttribute("data-aigui-form-instance", instanceId);
|
|
295
300
|
const controls = new Map();
|
|
296
301
|
const errorElements = new Map();
|
|
302
|
+
const fieldContainers = new Map();
|
|
297
303
|
for (const field of definition.fields) {
|
|
298
304
|
const rendered = createField(instancePrefix, field);
|
|
299
305
|
formElement.appendChild(rendered.container);
|
|
300
306
|
controls.set(field.name, rendered.control);
|
|
301
307
|
errorElements.set(field.name, rendered.error);
|
|
308
|
+
fieldContainers.set(field.name, rendered.container);
|
|
302
309
|
}
|
|
303
310
|
const actionError = document.createElement("div");
|
|
304
311
|
actionError.setAttribute("data-aigui-form-action-error", "");
|
|
305
312
|
actionError.setAttribute("role", "alert");
|
|
306
313
|
actionError.hidden = true;
|
|
307
314
|
formElement.appendChild(actionError);
|
|
315
|
+
const outcomeMessage = document.createElement("div");
|
|
316
|
+
outcomeMessage.setAttribute("data-aigui-form-outcome-message", "");
|
|
317
|
+
outcomeMessage.setAttribute("aria-live", "polite");
|
|
318
|
+
outcomeMessage.hidden = true;
|
|
319
|
+
formElement.appendChild(outcomeMessage);
|
|
308
320
|
const submit = document.createElement("button");
|
|
309
321
|
submit.type = "button";
|
|
310
322
|
submit.setAttribute("data-aigui-form-submit", "");
|
|
311
323
|
submit.textContent = definition.submitLabel ?? "Submit";
|
|
312
324
|
formElement.appendChild(submit);
|
|
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
|
+
/**
|
|
348
|
+
* Show how the submission turned out, when the handler judged it.
|
|
349
|
+
*
|
|
350
|
+
* The result used to be discarded, so an app that knew the answer was wrong had no way to say so
|
|
351
|
+
* — the form simply disabled itself and read "Submitted" whether the answer was right or not.
|
|
352
|
+
*/
|
|
353
|
+
const applyOutcome = (result, fallback) => {
|
|
354
|
+
const outcome = actionOutcome(result) ?? fallback;
|
|
355
|
+
if (!outcome || disposed) return;
|
|
356
|
+
formElement.setAttribute("data-aigui-form-outcome", outcome.tone);
|
|
357
|
+
if (outcome.message) {
|
|
358
|
+
outcomeMessage.textContent = outcome.message;
|
|
359
|
+
outcomeMessage.hidden = false;
|
|
360
|
+
}
|
|
361
|
+
for (const [name, tone] of Object.entries(outcome.fields ?? {})) fieldContainers.get(name)?.setAttribute("data-aigui-form-field-outcome", tone);
|
|
362
|
+
};
|
|
313
363
|
const markSubmitted = () => {
|
|
314
364
|
if (disposed) return;
|
|
315
365
|
pending = false;
|
|
@@ -338,6 +388,7 @@ function mountForm(host, definition, options) {
|
|
|
338
388
|
pending = true;
|
|
339
389
|
submit.disabled = true;
|
|
340
390
|
formElement.setAttribute("aria-busy", "true");
|
|
391
|
+
const graded = gradeAgainstExpectations(validation.values);
|
|
341
392
|
const settle = () => {
|
|
342
393
|
if (disposed) return;
|
|
343
394
|
pending = false;
|
|
@@ -351,7 +402,10 @@ function mountForm(host, definition, options) {
|
|
|
351
402
|
}, {
|
|
352
403
|
owner,
|
|
353
404
|
signal: controller.signal
|
|
354
|
-
}).then(
|
|
405
|
+
}).then((result) => {
|
|
406
|
+
markSubmitted();
|
|
407
|
+
applyOutcome(result, graded);
|
|
408
|
+
}, (error) => {
|
|
355
409
|
if (!disposed && !controller.signal.aborted) {
|
|
356
410
|
actionError.textContent = actionErrorMessage(error);
|
|
357
411
|
actionError.hidden = false;
|
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",
|