@iyulab/lowline-form 0.10.0 → 0.12.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/README.md +19 -1
- package/dist/bundle/lowline-form.bundle.js +1162 -1010
- package/dist/client.d.ts +19 -2
- package/dist/client.js +6 -3
- package/dist/draft.d.ts +29 -5
- package/dist/draft.js +38 -7
- package/dist/headless.d.ts +2 -2
- package/dist/lowline-form.d.ts +24 -3
- package/dist/lowline-form.js +54 -6
- package/dist/session.d.ts +54 -4
- package/dist/session.js +108 -10
- package/package.json +5 -5
package/dist/session.js
CHANGED
|
@@ -3,8 +3,20 @@ export const englishStrings = {
|
|
|
3
3
|
decline: 'Not this',
|
|
4
4
|
fromValue: 'As before with {field} {value}',
|
|
5
5
|
fromSimilar: 'As in a similar record',
|
|
6
|
-
fromJudgement: '
|
|
6
|
+
fromJudgement: 'Judged from this record’s values — no settled record answers it',
|
|
7
7
|
fromWriting: 'From what you wrote',
|
|
8
|
+
abstain: {
|
|
9
|
+
off: 'Yours to decide — this form suggests nothing here',
|
|
10
|
+
'no-evidence': 'No suggestion yet — nothing settled bears on it',
|
|
11
|
+
unsure: 'No suggestion — the records are not sure enough',
|
|
12
|
+
'not-allowed': 'No suggestion among the choices you may pick',
|
|
13
|
+
'needs-judgement': 'Needs your judgement — the records cannot tell',
|
|
14
|
+
'not-a-choice': 'No suggestion — the value given was not one of the choices',
|
|
15
|
+
malformed: 'No suggestion — the value given does not fit this field',
|
|
16
|
+
together: 'No suggestion until the fields decided with it are settled',
|
|
17
|
+
},
|
|
18
|
+
judgedNothing: 'Needs your judgement — neither the records nor a judgement gave a suggestion',
|
|
19
|
+
promise: 'right {right} times in 10 like this',
|
|
8
20
|
};
|
|
9
21
|
/**
|
|
10
22
|
* One record being written: its draft, and what its fields show. A field's answer may be held back once, at random,
|
|
@@ -12,7 +24,13 @@ export const englishStrings = {
|
|
|
12
24
|
* is looked at, and only for a value the decider suggests.
|
|
13
25
|
*/
|
|
14
26
|
export class Session {
|
|
15
|
-
|
|
27
|
+
/** Whether the record is being audited: `audit` is a probability above 0 and at most 1. */
|
|
28
|
+
get auditing() {
|
|
29
|
+
return this.audit > 0 && this.audit <= 1;
|
|
30
|
+
}
|
|
31
|
+
constructor(id, opened, rate = 0.05, random = Math.random,
|
|
32
|
+
/** The words the form shows; a host may change them while a record is written, and the next round says them. */
|
|
33
|
+
strings = englishStrings,
|
|
16
34
|
/** A steady clock in milliseconds (`performance.now()` by default): the record's times are read from it. */
|
|
17
35
|
clock = () => performance.now()) {
|
|
18
36
|
this.id = id;
|
|
@@ -25,8 +43,30 @@ export class Session {
|
|
|
25
43
|
this.filled = new Set();
|
|
26
44
|
/** What filling from the person's writing offers, by field, until the person picks it or turns it down. */
|
|
27
45
|
this.written = new Map();
|
|
46
|
+
/** Why filling from the person's writing gave a field nothing to offer, by field, until it offers one. */
|
|
47
|
+
this.unfilled = new Map();
|
|
28
48
|
/** The judgement asked for each field, by what it reads; `null` while the host is still judging. */
|
|
29
49
|
this.judgements = new Map();
|
|
50
|
+
/**
|
|
51
|
+
* Whether a suggestion the engine allows to go in unasked (`prefill`) is put in as the field's starting value. Off, it
|
|
52
|
+
* stays beside the field until the person takes it.
|
|
53
|
+
*/
|
|
54
|
+
this.prefills = true;
|
|
55
|
+
/**
|
|
56
|
+
* How often the promise of a suggestion is shown beside it, from 0 (never — the default) to 1 (always). Drawn once for
|
|
57
|
+
* each suggestion a field shows, and the draw goes into the draft — so the records can tell whether seeing it changed
|
|
58
|
+
* what people settled. A value outside 0 to 1 shows none.
|
|
59
|
+
*/
|
|
60
|
+
this.promises = 0;
|
|
61
|
+
/**
|
|
62
|
+
* The probability this record was drawn with to be settled again by a second person — an audit — or 0 (the default)
|
|
63
|
+
* for a record written as usual. Above 0 and at most 1, the person is shown nothing the first settler or the decider
|
|
64
|
+
* said: every decided field's answer is held back with this probability (a check), and no other suggestion, note or
|
|
65
|
+
* judgement is shown. The host draws the record and opens it with only the values it did not decide.
|
|
66
|
+
*/
|
|
67
|
+
this.audit = 0;
|
|
68
|
+
/** The promise draw of the suggestion each field shows: whether its promise is shown. */
|
|
69
|
+
this.promised = new Map();
|
|
30
70
|
this.draft = new Draft(opened);
|
|
31
71
|
this.draft.openAt(this.clock());
|
|
32
72
|
this.seen = { ...this.draft.opened };
|
|
@@ -63,29 +103,53 @@ export class Session {
|
|
|
63
103
|
if (proposal.suggestion && !this.written.has(proposal.field))
|
|
64
104
|
this.draw(proposal);
|
|
65
105
|
}
|
|
106
|
+
// Audited, the record shows nothing anyone said of it: answers that can be checked are held, the rest go unshown.
|
|
107
|
+
if (this.auditing)
|
|
108
|
+
return { fieldStates: {}, prefill: {} };
|
|
66
109
|
// A field held back for a check holds back the open fields of its group: the person settling it with nothing in view
|
|
67
110
|
// is not shown what is suggested alongside it. Only the held field is the check.
|
|
68
111
|
const held = proposals.filter((p) => this.draft.held(p.field) !== undefined);
|
|
69
112
|
const hidden = (p) => held.some((h) => h.field === p.field || (h.together != null && h.together === p.together));
|
|
70
113
|
for (const proposal of proposals) {
|
|
71
114
|
const { field, suggestion } = proposal;
|
|
72
|
-
if (
|
|
115
|
+
if (this.written.has(field))
|
|
73
116
|
continue;
|
|
74
117
|
if (hidden(proposal))
|
|
75
118
|
continue;
|
|
76
|
-
|
|
77
|
-
|
|
119
|
+
if (!suggestion) {
|
|
120
|
+
// Why the field is empty, said beside it: silent, it would read as a field nothing is suggested for, and a
|
|
121
|
+
// person passes over it. A field that already holds a value needs no word.
|
|
122
|
+
const judged = this.judgements.get(field)?.proposal === proposal;
|
|
123
|
+
// A field that asks for a judgement on nothing yet has not been looked at: there is nothing to say of it.
|
|
124
|
+
const early = proposal.abstain === 'needs-judgement' && (proposal.asks?.length ?? 0) === 0;
|
|
125
|
+
const why = this.unfilled.get(field) ??
|
|
126
|
+
(judged ? this.strings.judgedNothing : proposal.abstain && !early ? this.strings.abstain[proposal.abstain] : '');
|
|
127
|
+
if (why && isEmpty(values[field]))
|
|
128
|
+
round.fieldStates[field] = { note: why };
|
|
78
129
|
continue;
|
|
130
|
+
}
|
|
131
|
+
// Turned down, it stays down for this record — whatever was suggested between: neither shown again nor put back in.
|
|
132
|
+
// The draft takes it as the suggestion in view, turned down, so the record says what was last offered.
|
|
79
133
|
this.draft.show(field, suggestion, proposal);
|
|
134
|
+
if (this.draft.declined(field, suggestion))
|
|
135
|
+
continue;
|
|
136
|
+
// The answers that carry a promise say in the record whether it was shown beside them, and with what chance.
|
|
137
|
+
const promised = proposal.promise !== undefined && (suggestion.origin ?? 'decider') === 'decider';
|
|
138
|
+
const shown = promised && this.promiseShown(field, suggestion.value);
|
|
139
|
+
if (promised)
|
|
140
|
+
this.draft.showPromise(field, shown, this.promiseRate());
|
|
80
141
|
this.draft.reach(field, this.clock());
|
|
81
|
-
if (proposal.prefill && isEmpty(values[field])) {
|
|
142
|
+
if (this.prefills && proposal.prefill && isEmpty(values[field])) {
|
|
82
143
|
round.prefill[field] = suggestion.value;
|
|
83
144
|
this.filled.add(field);
|
|
84
145
|
}
|
|
146
|
+
const note = [this.note(proposal, values), shown ? this.promiseNote(proposal.promise) : undefined]
|
|
147
|
+
.filter((part) => part)
|
|
148
|
+
.join(' · ');
|
|
85
149
|
round.fieldStates[field] = {
|
|
86
150
|
suggestions: [suggestion.value],
|
|
87
151
|
decline: this.strings.decline,
|
|
88
|
-
...(
|
|
152
|
+
...(note ? { note } : {}),
|
|
89
153
|
};
|
|
90
154
|
}
|
|
91
155
|
return round;
|
|
@@ -138,14 +202,21 @@ export class Session {
|
|
|
138
202
|
* on its own, in place of what the records suggest for the field. Answers what every field shows now.
|
|
139
203
|
*/
|
|
140
204
|
applyFill(proposals) {
|
|
141
|
-
for (const { field, suggestion } of proposals) {
|
|
142
|
-
if (!suggestion)
|
|
205
|
+
for (const { field, suggestion, abstain } of proposals) {
|
|
206
|
+
if (!suggestion) {
|
|
207
|
+
// A value was given for the field that it cannot take: said beside the field, not dropped in silence.
|
|
208
|
+
const why = abstain ? this.strings.abstain[abstain] : '';
|
|
209
|
+
if (why)
|
|
210
|
+
this.unfilled.set(field, why);
|
|
143
211
|
continue;
|
|
212
|
+
}
|
|
213
|
+
this.unfilled.delete(field);
|
|
144
214
|
this.draft.show(field, suggestion);
|
|
145
215
|
this.filled.delete(field);
|
|
146
216
|
this.written.set(field, { suggestions: [suggestion.value], decline: this.strings.decline, note: this.strings.fromWriting });
|
|
147
217
|
}
|
|
148
|
-
|
|
218
|
+
const unfilled = [...this.unfilled].filter(([field]) => !this.written.has(field));
|
|
219
|
+
return { ...Object.fromEntries(this.written), ...Object.fromEntries(unfilled.map(([field, note]) => [field, { note }])) };
|
|
149
220
|
}
|
|
150
221
|
/** The person picked a field's offered value: what was offered from their writing has done its part. */
|
|
151
222
|
picked(field) {
|
|
@@ -170,6 +241,7 @@ export class Session {
|
|
|
170
241
|
this.draft.openAt(this.clock());
|
|
171
242
|
this.seen = { ...this.draft.opened };
|
|
172
243
|
this.drawn.clear();
|
|
244
|
+
this.promised.clear();
|
|
173
245
|
this.filled.clear();
|
|
174
246
|
this.written.clear();
|
|
175
247
|
this.judgements.clear();
|
|
@@ -185,12 +257,38 @@ export class Session {
|
|
|
185
257
|
if (!checkable)
|
|
186
258
|
return false;
|
|
187
259
|
this.drawn.add(field);
|
|
260
|
+
if (this.auditing) {
|
|
261
|
+
this.draft.hold(field, this.audit, { ...proposal, suggestion });
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
188
264
|
if (this.rate > 0 && this.rate <= 1 && this.random() < this.rate) {
|
|
189
265
|
this.draft.hold(field, this.rate, { ...proposal, suggestion });
|
|
190
266
|
return true;
|
|
191
267
|
}
|
|
192
268
|
return false;
|
|
193
269
|
}
|
|
270
|
+
/** How often promises are shown: `promises` when it is one, else never. */
|
|
271
|
+
promiseRate() {
|
|
272
|
+
return this.promises >= 0 && this.promises <= 1 ? this.promises : 0;
|
|
273
|
+
}
|
|
274
|
+
/** Whether the promise of the field's suggestion `value` is shown — drawn the first time that suggestion is shown. */
|
|
275
|
+
promiseShown(field, value) {
|
|
276
|
+
const drawn = this.promised.get(field);
|
|
277
|
+
if (drawn?.value === value)
|
|
278
|
+
return drawn.shown;
|
|
279
|
+
const rate = this.promiseRate();
|
|
280
|
+
const shown = rate >= 1 || (rate > 0 && this.random() < rate);
|
|
281
|
+
this.promised.set(field, { value, shown });
|
|
282
|
+
return shown;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* A promise as a count out of ten, rounded down: a promise is never said higher than the records bear it out, and a
|
|
286
|
+
* count reads more plainly than a percentage.
|
|
287
|
+
*/
|
|
288
|
+
promiseNote(promise) {
|
|
289
|
+
const right = Math.max(0, Math.min(10, Math.floor(promise * 10 + 1e-9)));
|
|
290
|
+
return this.strings.promise.replace('{right}', String(right));
|
|
291
|
+
}
|
|
194
292
|
/** Where the suggestion comes from, in words. A field's value is shown as the record holds it now. */
|
|
195
293
|
note(proposal, values) {
|
|
196
294
|
const first = proposal.basis?.[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/lowline-form",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "The Lowline form element: a formdown form whose decided fields are suggested from the records people confirmed, talking to a Lowline host's routes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"bundle": "vite build --config vite.bundle.config.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@formdown/ui": "^0.26.
|
|
43
|
+
"@formdown/ui": "^0.26.1",
|
|
44
44
|
"lit": "^3.3.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"typescript": "^
|
|
48
|
-
"vite": "^8.3.
|
|
49
|
-
"vitest": "^5.0
|
|
47
|
+
"typescript": "^7.0.2",
|
|
48
|
+
"vite": "^8.3.6",
|
|
49
|
+
"vitest": "^5.1.0"
|
|
50
50
|
}
|
|
51
51
|
}
|