@iyulab/lowline-form 0.0.0-stage → 0.1.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 +39 -2
- package/dist/bundle/lowline-form.bundle.js +11205 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +52 -0
- package/dist/draft.d.ts +50 -0
- package/dist/draft.js +74 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lowline-form.d.ts +71 -0
- package/dist/lowline-form.js +212 -0
- package/dist/session.d.ts +111 -0
- package/dist/session.js +173 -0
- package/package.json +40 -4
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Draft, Values } from './draft.js';
|
|
2
|
+
import type { Proposal } from './session.js';
|
|
3
|
+
/** A route of the host refused a call: its status, and what it said. */
|
|
4
|
+
export declare class LowlineError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
constructor(status: number, message: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The routes a Lowline host maps (`MapLowline` in .NET): `POST {src}/{form}/propose` and `POST {src}/{form}/settle`,
|
|
10
|
+
* and — when the host gave a model — `fill` and `judge`. The host decides the memory space and the choices a person may pick from the request
|
|
11
|
+
* itself — its sign-in — so the calls carry the record only.
|
|
12
|
+
*/
|
|
13
|
+
export declare class LowlineClient {
|
|
14
|
+
readonly src: string;
|
|
15
|
+
private readonly fetcher;
|
|
16
|
+
constructor(src: string, fetcher?: typeof fetch);
|
|
17
|
+
/** What to show for each open decided field of record `id`, given its values so far. */
|
|
18
|
+
propose(form: string, id: string, values: Values): Promise<Proposal[]>;
|
|
19
|
+
/** Saves record `id` holding `saved`, with the draft kept while it was written. */
|
|
20
|
+
settle(form: string, id: string, saved: Values, draft: Draft): Promise<{
|
|
21
|
+
id: string;
|
|
22
|
+
settledAt: string;
|
|
23
|
+
}>;
|
|
24
|
+
/** What the host's model makes of `text` for the fields of record values still empty — 404 when the host gave none. */
|
|
25
|
+
fill(form: string, values: Values, text: string): Promise<Proposal[]>;
|
|
26
|
+
/**
|
|
27
|
+
* What the host's model judged for the decided fields of record `id` the records could not answer
|
|
28
|
+
* (`needs-judgement`) — 404 when the host gave no model.
|
|
29
|
+
*/
|
|
30
|
+
judge(form: string, id: string, values: Values): Promise<Proposal[]>;
|
|
31
|
+
private post;
|
|
32
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** A route of the host refused a call: its status, and what it said. */
|
|
2
|
+
export class LowlineError extends Error {
|
|
3
|
+
constructor(status, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = 'LowlineError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The routes a Lowline host maps (`MapLowline` in .NET): `POST {src}/{form}/propose` and `POST {src}/{form}/settle`,
|
|
11
|
+
* and — when the host gave a model — `fill` and `judge`. The host decides the memory space and the choices a person may pick from the request
|
|
12
|
+
* itself — its sign-in — so the calls carry the record only.
|
|
13
|
+
*/
|
|
14
|
+
export class LowlineClient {
|
|
15
|
+
constructor(src, fetcher = (...args) => fetch(...args)) {
|
|
16
|
+
this.src = src;
|
|
17
|
+
this.fetcher = fetcher;
|
|
18
|
+
}
|
|
19
|
+
/** What to show for each open decided field of record `id`, given its values so far. */
|
|
20
|
+
propose(form, id, values) {
|
|
21
|
+
return this.post(form, 'propose', { id, values });
|
|
22
|
+
}
|
|
23
|
+
/** Saves record `id` holding `saved`, with the draft kept while it was written. */
|
|
24
|
+
settle(form, id, saved, draft) {
|
|
25
|
+
return this.post(form, 'settle', { id, saved, draft });
|
|
26
|
+
}
|
|
27
|
+
/** What the host's model makes of `text` for the fields of record values still empty — 404 when the host gave none. */
|
|
28
|
+
fill(form, values, text) {
|
|
29
|
+
return this.post(form, 'fill', { values, text });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* What the host's model judged for the decided fields of record `id` the records could not answer
|
|
33
|
+
* (`needs-judgement`) — 404 when the host gave no model.
|
|
34
|
+
*/
|
|
35
|
+
judge(form, id, values) {
|
|
36
|
+
return this.post(form, 'judge', { id, values });
|
|
37
|
+
}
|
|
38
|
+
async post(form, route, body) {
|
|
39
|
+
const url = `${this.src.replace(/\/+$/, '')}/${encodeURIComponent(form)}/${route}`;
|
|
40
|
+
const response = await this.fetcher(url, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'content-type': 'application/json' },
|
|
43
|
+
body: JSON.stringify(body),
|
|
44
|
+
credentials: 'same-origin',
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const problem = await response.json().catch(() => undefined);
|
|
48
|
+
throw new LowlineError(response.status, problem?.detail ?? `${route} answered ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
return (await response.json());
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/draft.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** What a field holds: one text, or several for a field that takes several. */
|
|
2
|
+
export type Value = string | string[];
|
|
3
|
+
/** A record's values by field. */
|
|
4
|
+
export type Values = Record<string, Value>;
|
|
5
|
+
/** A suggestion shown for a field: a value, or — `fit: 'prefix'` — a class the value begins with. */
|
|
6
|
+
export interface Suggestion {
|
|
7
|
+
value: string;
|
|
8
|
+
origin?: 'decider' | 'other';
|
|
9
|
+
fit?: 'exact' | 'prefix';
|
|
10
|
+
}
|
|
11
|
+
interface Track {
|
|
12
|
+
shown?: Suggestion;
|
|
13
|
+
declined?: boolean;
|
|
14
|
+
held?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The account of one record while it is written: the values it was opened with, and for each field the suggestion
|
|
18
|
+
* last shown, whether it was turned down, and whether its answer was held back to be checked. The host's engine
|
|
19
|
+
* turns it, with the saved values, into what each decided field teaches — so a value taken from a suggestion is never
|
|
20
|
+
* counted as one typed. It is only bookkeeping: nothing is counted here.
|
|
21
|
+
*
|
|
22
|
+
* Written as the engine reads it: `{ opened, fields: { field: { shown, declined, held } } }`.
|
|
23
|
+
*/
|
|
24
|
+
export declare class Draft {
|
|
25
|
+
readonly opened: Values;
|
|
26
|
+
readonly fields: Record<string, Track>;
|
|
27
|
+
/** A record opened holding `values` — none for a new one. Empty values are left out. */
|
|
28
|
+
constructor(values?: Values);
|
|
29
|
+
/** A suggestion came into view for the field. A suggestion turned down earlier stays turned down only if it is this. */
|
|
30
|
+
show(field: string, suggestion: Suggestion): void;
|
|
31
|
+
/** The field's answer is held back for a check, drawn with `probability`: what the person settles is checked. */
|
|
32
|
+
hold(field: string, probability: number): void;
|
|
33
|
+
/** The person turned down the suggestion in view; false, changing nothing, when none was shown. */
|
|
34
|
+
decline(field: string): boolean;
|
|
35
|
+
/** Whether `suggestion` is the one in view for the field and the person turned it down. */
|
|
36
|
+
declined(field: string, suggestion: Suggestion): boolean;
|
|
37
|
+
shown(field: string): Suggestion | undefined;
|
|
38
|
+
held(field: string): number | undefined;
|
|
39
|
+
toJSON(): {
|
|
40
|
+
opened: Values;
|
|
41
|
+
fields: Record<string, Track>;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export declare function isEmpty(value: Value | undefined): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* A form's data as the engine takes it: texts, and lists of texts; a number or a check as its text. Empty fields are
|
|
47
|
+
* left out.
|
|
48
|
+
*/
|
|
49
|
+
export declare function valuesOf(data: Record<string, unknown>): Values;
|
|
50
|
+
export {};
|
package/dist/draft.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account of one record while it is written: the values it was opened with, and for each field the suggestion
|
|
3
|
+
* last shown, whether it was turned down, and whether its answer was held back to be checked. The host's engine
|
|
4
|
+
* turns it, with the saved values, into what each decided field teaches — so a value taken from a suggestion is never
|
|
5
|
+
* counted as one typed. It is only bookkeeping: nothing is counted here.
|
|
6
|
+
*
|
|
7
|
+
* Written as the engine reads it: `{ opened, fields: { field: { shown, declined, held } } }`.
|
|
8
|
+
*/
|
|
9
|
+
export class Draft {
|
|
10
|
+
/** A record opened holding `values` — none for a new one. Empty values are left out. */
|
|
11
|
+
constructor(values = {}) {
|
|
12
|
+
this.fields = {};
|
|
13
|
+
this.opened = Object.fromEntries(Object.entries(values).filter(([, v]) => !isEmpty(v)));
|
|
14
|
+
}
|
|
15
|
+
/** A suggestion came into view for the field. A suggestion turned down earlier stays turned down only if it is this. */
|
|
16
|
+
show(field, suggestion) {
|
|
17
|
+
const track = (this.fields[field] ??= {});
|
|
18
|
+
if (!sameSuggestion(track.shown, suggestion))
|
|
19
|
+
track.declined = false;
|
|
20
|
+
track.shown = { ...suggestion };
|
|
21
|
+
delete track.held;
|
|
22
|
+
}
|
|
23
|
+
/** The field's answer is held back for a check, drawn with `probability`: what the person settles is checked. */
|
|
24
|
+
hold(field, probability) {
|
|
25
|
+
if (!(probability > 0 && probability <= 1))
|
|
26
|
+
throw new RangeError('a check is drawn with a probability above 0 and at most 1');
|
|
27
|
+
this.fields[field] = { held: probability };
|
|
28
|
+
}
|
|
29
|
+
/** The person turned down the suggestion in view; false, changing nothing, when none was shown. */
|
|
30
|
+
decline(field) {
|
|
31
|
+
const track = this.fields[field];
|
|
32
|
+
if (!track?.shown)
|
|
33
|
+
return false;
|
|
34
|
+
track.declined = true;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/** Whether `suggestion` is the one in view for the field and the person turned it down. */
|
|
38
|
+
declined(field, suggestion) {
|
|
39
|
+
const track = this.fields[field];
|
|
40
|
+
return track?.declined === true && sameSuggestion(track.shown, suggestion);
|
|
41
|
+
}
|
|
42
|
+
shown(field) {
|
|
43
|
+
return this.fields[field]?.shown;
|
|
44
|
+
}
|
|
45
|
+
held(field) {
|
|
46
|
+
return this.fields[field]?.held;
|
|
47
|
+
}
|
|
48
|
+
toJSON() {
|
|
49
|
+
return { opened: this.opened, fields: this.fields };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function sameSuggestion(a, b) {
|
|
53
|
+
return a !== undefined && a.value === b.value && (a.origin ?? 'decider') === (b.origin ?? 'decider') && (a.fit ?? 'exact') === (b.fit ?? 'exact');
|
|
54
|
+
}
|
|
55
|
+
export function isEmpty(value) {
|
|
56
|
+
return value === undefined || (Array.isArray(value) ? value.every((v) => v === '') : value === '');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A form's data as the engine takes it: texts, and lists of texts; a number or a check as its text. Empty fields are
|
|
60
|
+
* left out.
|
|
61
|
+
*/
|
|
62
|
+
export function valuesOf(data) {
|
|
63
|
+
const values = {};
|
|
64
|
+
for (const [field, raw] of Object.entries(data)) {
|
|
65
|
+
const value = Array.isArray(raw)
|
|
66
|
+
? raw.map(String)
|
|
67
|
+
: raw === null || raw === undefined
|
|
68
|
+
? undefined
|
|
69
|
+
: String(raw);
|
|
70
|
+
if (value !== undefined && !isEmpty(value))
|
|
71
|
+
values[field] = value;
|
|
72
|
+
}
|
|
73
|
+
return values;
|
|
74
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Draft, valuesOf, type Suggestion, type Value, type Values } from './draft.js';
|
|
2
|
+
export { Session, englishStrings, type Abstain, type Basis, type FieldState, type Proposal, type Round, type Strings } from './session.js';
|
|
3
|
+
export { LowlineClient, LowlineError } from './client.js';
|
|
4
|
+
export { LowlineForm } from './lowline-form.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import '@formdown/ui';
|
|
3
|
+
import { type Values } from './draft.js';
|
|
4
|
+
import { type Strings } from './session.js';
|
|
5
|
+
/**
|
|
6
|
+
* A formdown form whose decided fields are suggested from the records people confirmed. It asks the host's routes
|
|
7
|
+
* (`src` — where `MapLowline` put them) what to show for form `form` while record `record` is written, draws each
|
|
8
|
+
* suggestion in its field, keeps the account of what the person took or turned down, and sends it with the save.
|
|
9
|
+
*
|
|
10
|
+
* ```html
|
|
11
|
+
* <lowline-form src="/lowline" form="intake" record="r-17"></lowline-form>
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Set `content` to the form's formdown text and, for a record opened again, `data` to its values. `save()` saves it;
|
|
15
|
+
* `lowline-saved` follows with `{ id, settledAt, values }`.
|
|
16
|
+
*/
|
|
17
|
+
export declare class LowlineForm extends LitElement {
|
|
18
|
+
/** Where the host's routes are. */
|
|
19
|
+
src: string;
|
|
20
|
+
/** The form, by the name the host defined it under. */
|
|
21
|
+
form: string;
|
|
22
|
+
/** The record being written; opening a record again under its id keeps it from being evidence for itself. */
|
|
23
|
+
record: string;
|
|
24
|
+
/** The form's formdown text. */
|
|
25
|
+
content: string;
|
|
26
|
+
/** The record's values when it was opened. */
|
|
27
|
+
data: Record<string, unknown>;
|
|
28
|
+
/** How often a field's answer is held back to check it, from 0 to 1. */
|
|
29
|
+
rate: number;
|
|
30
|
+
/** The words the form shows, in the host's language. */
|
|
31
|
+
strings: Strings;
|
|
32
|
+
private formdown?;
|
|
33
|
+
private session?;
|
|
34
|
+
private asking?;
|
|
35
|
+
private client;
|
|
36
|
+
private fieldStates;
|
|
37
|
+
/** Whether the host judges what the records could not answer — until its `judge` route says it has no model. */
|
|
38
|
+
private canJudge;
|
|
39
|
+
protected createRenderRoot(): this;
|
|
40
|
+
protected willUpdate(changed: Map<PropertyKey, unknown>): void;
|
|
41
|
+
protected updated(changed: Map<PropertyKey, unknown>): void;
|
|
42
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
43
|
+
/** The values the form holds now. */
|
|
44
|
+
values(): Values;
|
|
45
|
+
/** Saves the record as it stands, with the account of what was shown and done; what is written next starts here. */
|
|
46
|
+
save(): Promise<{
|
|
47
|
+
id: string;
|
|
48
|
+
settledAt: string;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Fills the record from `text` — what the person wrote or a picture's description — through the host's model: each
|
|
52
|
+
* value it gives is offered in its field for the person to take. Nothing is put in on its own.
|
|
53
|
+
*/
|
|
54
|
+
fill(text: string): Promise<void>;
|
|
55
|
+
/** Asks again what to show for the decided fields still open. */
|
|
56
|
+
ask(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Asks the host to judge the fields of a round the records could not answer — apart from the round, since a model
|
|
59
|
+
* takes longer — and asks again once it has, so the judgements are shown in their fields.
|
|
60
|
+
*/
|
|
61
|
+
private judge;
|
|
62
|
+
private changed;
|
|
63
|
+
private picked;
|
|
64
|
+
private declined;
|
|
65
|
+
disconnectedCallback(): void;
|
|
66
|
+
}
|
|
67
|
+
declare global {
|
|
68
|
+
interface HTMLElementTagNameMap {
|
|
69
|
+
'lowline-form': LowlineForm;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, html } from 'lit';
|
|
8
|
+
import { customElement, property, query } from 'lit/decorators.js';
|
|
9
|
+
import '@formdown/ui';
|
|
10
|
+
import { LowlineClient, LowlineError } from './client.js';
|
|
11
|
+
import { valuesOf } from './draft.js';
|
|
12
|
+
import { Session, englishStrings } from './session.js';
|
|
13
|
+
/** How long typing pauses before the decided fields are asked about again. */
|
|
14
|
+
const ASK_DELAY_MS = 300;
|
|
15
|
+
/**
|
|
16
|
+
* A formdown form whose decided fields are suggested from the records people confirmed. It asks the host's routes
|
|
17
|
+
* (`src` — where `MapLowline` put them) what to show for form `form` while record `record` is written, draws each
|
|
18
|
+
* suggestion in its field, keeps the account of what the person took or turned down, and sends it with the save.
|
|
19
|
+
*
|
|
20
|
+
* ```html
|
|
21
|
+
* <lowline-form src="/lowline" form="intake" record="r-17"></lowline-form>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Set `content` to the form's formdown text and, for a record opened again, `data` to its values. `save()` saves it;
|
|
25
|
+
* `lowline-saved` follows with `{ id, settledAt, values }`.
|
|
26
|
+
*/
|
|
27
|
+
let LowlineForm = class LowlineForm extends LitElement {
|
|
28
|
+
constructor() {
|
|
29
|
+
super(...arguments);
|
|
30
|
+
/** Where the host's routes are. */
|
|
31
|
+
this.src = '/lowline';
|
|
32
|
+
/** The form, by the name the host defined it under. */
|
|
33
|
+
this.form = '';
|
|
34
|
+
/** The record being written; opening a record again under its id keeps it from being evidence for itself. */
|
|
35
|
+
this.record = '';
|
|
36
|
+
/** The form's formdown text. */
|
|
37
|
+
this.content = '';
|
|
38
|
+
/** The record's values when it was opened. */
|
|
39
|
+
this.data = {};
|
|
40
|
+
/** How often a field's answer is held back to check it, from 0 to 1. */
|
|
41
|
+
this.rate = 0.05;
|
|
42
|
+
/** The words the form shows, in the host's language. */
|
|
43
|
+
this.strings = englishStrings;
|
|
44
|
+
this.client = new LowlineClient(this.src);
|
|
45
|
+
this.fieldStates = {};
|
|
46
|
+
/** Whether the host judges what the records could not answer — until its `judge` route says it has no model. */
|
|
47
|
+
this.canJudge = true;
|
|
48
|
+
this.changed = () => {
|
|
49
|
+
clearTimeout(this.asking);
|
|
50
|
+
this.asking = setTimeout(() => void this.ask(), ASK_DELAY_MS);
|
|
51
|
+
};
|
|
52
|
+
this.picked = (event) => {
|
|
53
|
+
const { field, value } = event.detail;
|
|
54
|
+
this.session?.picked(field);
|
|
55
|
+
const formdown = this.formdown;
|
|
56
|
+
if (formdown)
|
|
57
|
+
formdown.updateData({ ...formdown.data, [field]: value });
|
|
58
|
+
};
|
|
59
|
+
this.declined = (event) => {
|
|
60
|
+
const { field } = event.detail;
|
|
61
|
+
const formdown = this.formdown;
|
|
62
|
+
if (this.session?.decline(field, this.values()) && formdown)
|
|
63
|
+
formdown.updateData({ ...formdown.data, [field]: '' });
|
|
64
|
+
const { [field]: _, ...rest } = this.fieldStates;
|
|
65
|
+
this.fieldStates = rest;
|
|
66
|
+
if (formdown)
|
|
67
|
+
formdown.fieldStates = rest;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
createRenderRoot() {
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
willUpdate(changed) {
|
|
74
|
+
if (changed.has('src')) {
|
|
75
|
+
this.client = new LowlineClient(this.src);
|
|
76
|
+
this.canJudge = true;
|
|
77
|
+
}
|
|
78
|
+
if (changed.has('record') || changed.has('form') || changed.has('data') || changed.has('content')) {
|
|
79
|
+
// The values it was opened with are taken before anything is suggested or put in.
|
|
80
|
+
this.session = new Session(this.record, valuesOf(this.data), this.rate, Math.random, this.strings);
|
|
81
|
+
this.fieldStates = {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
updated(changed) {
|
|
85
|
+
if (changed.has('record') || changed.has('form') || changed.has('content'))
|
|
86
|
+
void this.ask();
|
|
87
|
+
}
|
|
88
|
+
render() {
|
|
89
|
+
return html `<formdown-ui
|
|
90
|
+
.content=${this.content}
|
|
91
|
+
.data=${this.data}
|
|
92
|
+
.fieldStates=${this.fieldStates}
|
|
93
|
+
@formdown-change=${this.changed}
|
|
94
|
+
@formdown-suggestion-pick=${this.picked}
|
|
95
|
+
@formdown-suggestion-decline=${this.declined}
|
|
96
|
+
></formdown-ui>`;
|
|
97
|
+
}
|
|
98
|
+
/** The values the form holds now. */
|
|
99
|
+
values() {
|
|
100
|
+
return valuesOf(this.formdown?.data ?? this.data);
|
|
101
|
+
}
|
|
102
|
+
/** Saves the record as it stands, with the account of what was shown and done; what is written next starts here. */
|
|
103
|
+
async save() {
|
|
104
|
+
if (!this.session || !this.form || !this.record)
|
|
105
|
+
throw new Error('lowline-form needs a form and a record to save');
|
|
106
|
+
const values = this.values();
|
|
107
|
+
const saved = await this.client.settle(this.form, this.record, values, this.session.draft);
|
|
108
|
+
this.session.saved(values);
|
|
109
|
+
this.dispatchEvent(new CustomEvent('lowline-saved', { detail: { ...saved, values }, bubbles: true, composed: true }));
|
|
110
|
+
return saved;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Fills the record from `text` — what the person wrote or a picture's description — through the host's model: each
|
|
114
|
+
* value it gives is offered in its field for the person to take. Nothing is put in on its own.
|
|
115
|
+
*/
|
|
116
|
+
async fill(text) {
|
|
117
|
+
const session = this.session;
|
|
118
|
+
if (!session || !this.form)
|
|
119
|
+
return;
|
|
120
|
+
const proposals = await this.client.fill(this.form, this.values(), text);
|
|
121
|
+
if (session !== this.session)
|
|
122
|
+
return;
|
|
123
|
+
this.fieldStates = session.applyFill(proposals);
|
|
124
|
+
if (this.formdown)
|
|
125
|
+
this.formdown.fieldStates = this.fieldStates;
|
|
126
|
+
}
|
|
127
|
+
/** Asks again what to show for the decided fields still open. */
|
|
128
|
+
async ask() {
|
|
129
|
+
const session = this.session;
|
|
130
|
+
if (!session || !this.form || !this.record)
|
|
131
|
+
return;
|
|
132
|
+
const values = this.values();
|
|
133
|
+
let proposals;
|
|
134
|
+
try {
|
|
135
|
+
proposals = await this.client.propose(this.form, this.record, values);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Suggestions are an aid: without them the form is filled in by hand.
|
|
139
|
+
this.dispatchEvent(new CustomEvent('lowline-unavailable', { detail: { error }, bubbles: true, composed: true }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (session !== this.session)
|
|
143
|
+
return;
|
|
144
|
+
const shown = session.judged(proposals);
|
|
145
|
+
const round = session.apply(shown, values);
|
|
146
|
+
this.fieldStates = round.fieldStates;
|
|
147
|
+
const formdown = this.formdown;
|
|
148
|
+
if (!formdown)
|
|
149
|
+
return;
|
|
150
|
+
if (Object.keys(round.prefill).length > 0)
|
|
151
|
+
formdown.updateData({ ...formdown.data, ...round.prefill });
|
|
152
|
+
formdown.fieldStates = this.fieldStates;
|
|
153
|
+
if (this.canJudge)
|
|
154
|
+
void this.judge(session, values, shown);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Asks the host to judge the fields of a round the records could not answer — apart from the round, since a model
|
|
158
|
+
* takes longer — and asks again once it has, so the judgements are shown in their fields.
|
|
159
|
+
*/
|
|
160
|
+
async judge(session, values, round) {
|
|
161
|
+
const fields = session.toJudge(round);
|
|
162
|
+
if (fields.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
const asked = round.filter((p) => fields.includes(p.field));
|
|
165
|
+
let judged;
|
|
166
|
+
try {
|
|
167
|
+
judged = await this.client.judge(this.form, this.record, values);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof LowlineError && error.status === 404)
|
|
171
|
+
this.canJudge = false;
|
|
172
|
+
session.notJudged(asked);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (session !== this.session)
|
|
176
|
+
return;
|
|
177
|
+
session.takeJudged(asked, judged);
|
|
178
|
+
void this.ask();
|
|
179
|
+
}
|
|
180
|
+
disconnectedCallback() {
|
|
181
|
+
super.disconnectedCallback();
|
|
182
|
+
clearTimeout(this.asking);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
__decorate([
|
|
186
|
+
property()
|
|
187
|
+
], LowlineForm.prototype, "src", void 0);
|
|
188
|
+
__decorate([
|
|
189
|
+
property()
|
|
190
|
+
], LowlineForm.prototype, "form", void 0);
|
|
191
|
+
__decorate([
|
|
192
|
+
property()
|
|
193
|
+
], LowlineForm.prototype, "record", void 0);
|
|
194
|
+
__decorate([
|
|
195
|
+
property()
|
|
196
|
+
], LowlineForm.prototype, "content", void 0);
|
|
197
|
+
__decorate([
|
|
198
|
+
property({ attribute: false })
|
|
199
|
+
], LowlineForm.prototype, "data", void 0);
|
|
200
|
+
__decorate([
|
|
201
|
+
property({ type: Number })
|
|
202
|
+
], LowlineForm.prototype, "rate", void 0);
|
|
203
|
+
__decorate([
|
|
204
|
+
property({ attribute: false })
|
|
205
|
+
], LowlineForm.prototype, "strings", void 0);
|
|
206
|
+
__decorate([
|
|
207
|
+
query('formdown-ui')
|
|
208
|
+
], LowlineForm.prototype, "formdown", void 0);
|
|
209
|
+
LowlineForm = __decorate([
|
|
210
|
+
customElement('lowline-form')
|
|
211
|
+
], LowlineForm);
|
|
212
|
+
export { LowlineForm };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Draft, type Suggestion, type Values } from './draft.js';
|
|
2
|
+
/** What a suggestion rests on: another field's value it was settled under, or a settled record like this one. */
|
|
3
|
+
export type Basis = {
|
|
4
|
+
kind: 'value';
|
|
5
|
+
field: string;
|
|
6
|
+
value: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'record';
|
|
9
|
+
id: string;
|
|
10
|
+
similarity: number;
|
|
11
|
+
};
|
|
12
|
+
/** Why a decided field is shown no suggestion. */
|
|
13
|
+
export type Abstain = 'off' | 'no-evidence' | 'unsure' | 'not-allowed' | 'needs-judgement' | 'not-a-choice' | 'malformed';
|
|
14
|
+
/** What the host's engine says to show for one decided field. */
|
|
15
|
+
export interface Proposal {
|
|
16
|
+
field: string;
|
|
17
|
+
suggestion: Suggestion | null;
|
|
18
|
+
/** Whether the suggestion may go in as the field's value without the person taking it. */
|
|
19
|
+
prefill: boolean;
|
|
20
|
+
layer: 'key' | 'similar' | 'judgement' | null;
|
|
21
|
+
basis?: Basis[];
|
|
22
|
+
abstain: Abstain | null;
|
|
23
|
+
asks?: [string, string][];
|
|
24
|
+
}
|
|
25
|
+
/** What a field shows, as formdown's `fieldStates` takes it. */
|
|
26
|
+
export interface FieldState {
|
|
27
|
+
suggestions?: string[];
|
|
28
|
+
note?: string;
|
|
29
|
+
decline?: string;
|
|
30
|
+
label?: string;
|
|
31
|
+
}
|
|
32
|
+
/** The words the form shows, in the host's language. */
|
|
33
|
+
export interface Strings {
|
|
34
|
+
/** The button that turns the suggestion down. */
|
|
35
|
+
decline: string;
|
|
36
|
+
/** The note of a suggestion settled before under another field's value: `{field}` and `{value}` are filled in. */
|
|
37
|
+
fromValue: string;
|
|
38
|
+
/** The note of a suggestion taken from a similar record. */
|
|
39
|
+
fromSimilar: string;
|
|
40
|
+
/** The note of a suggestion from a judgement made outside the records. */
|
|
41
|
+
fromJudgement: string;
|
|
42
|
+
/** The note of a value filled in from what the person wrote. */
|
|
43
|
+
fromWriting: string;
|
|
44
|
+
}
|
|
45
|
+
export declare const englishStrings: Strings;
|
|
46
|
+
/** What the form does with one round of proposals: what each field shows, and the values to put in unasked. */
|
|
47
|
+
export interface Round {
|
|
48
|
+
fieldStates: Record<string, FieldState>;
|
|
49
|
+
prefill: Values;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One record being written: its draft, and what its fields show. A field's answer may be held back once, at random,
|
|
53
|
+
* to check it — `rate` is how often (one in twenty by default), drawn once per field of the record, before its answer
|
|
54
|
+
* is looked at, and only for a value the decider suggests.
|
|
55
|
+
*/
|
|
56
|
+
export declare class Session {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly rate: number;
|
|
59
|
+
private readonly random;
|
|
60
|
+
private readonly strings;
|
|
61
|
+
draft: Draft;
|
|
62
|
+
private drawn;
|
|
63
|
+
/** Fields whose value the form put in unasked, from the suggestion still shown. */
|
|
64
|
+
private filled;
|
|
65
|
+
/** What filling from the person's writing offers, by field, until the person picks it or turns it down. */
|
|
66
|
+
private written;
|
|
67
|
+
/** The judgement asked for each field, by what it reads; `null` while the host is still judging. */
|
|
68
|
+
private judgements;
|
|
69
|
+
constructor(id: string, opened: Values, rate?: number, random?: () => number, strings?: Strings);
|
|
70
|
+
/**
|
|
71
|
+
* Takes a round of proposals for the form's `values` now. A field offered a value from the person's writing keeps it
|
|
72
|
+
* in view until the person picks it or turns it down.
|
|
73
|
+
*/
|
|
74
|
+
apply(proposals: Proposal[], values: Values): Round;
|
|
75
|
+
/**
|
|
76
|
+
* The round's proposals with each field that asks for a judgement answered by the one already judged on the same
|
|
77
|
+
* values, if there is one — so a judgement stays in view while the person writes on, and is not asked for again.
|
|
78
|
+
*/
|
|
79
|
+
judged(proposals: Proposal[]): Proposal[];
|
|
80
|
+
/**
|
|
81
|
+
* The fields of the round that ask for a judgement not asked for yet on what they read; marked as being asked. A field
|
|
82
|
+
* with nothing to read yet is left until there is something.
|
|
83
|
+
*/
|
|
84
|
+
toJudge(proposals: Proposal[]): string[];
|
|
85
|
+
/**
|
|
86
|
+
* Takes what the host judged for the fields `asked` — each a proposal as the round would give it; a field it gave
|
|
87
|
+
* nothing for stays as it was, and is not asked about again on the same values. A field asked for again since, on
|
|
88
|
+
* other values, keeps waiting for that one.
|
|
89
|
+
*/
|
|
90
|
+
takeJudged(asked: Proposal[], judged: Proposal[]): void;
|
|
91
|
+
/** The host could not judge the fields `asked`: they may be asked about again. */
|
|
92
|
+
notJudged(asked: Proposal[]): void;
|
|
93
|
+
/**
|
|
94
|
+
* Takes what filling from the person's writing offers: each value shown as a suggestion from elsewhere, never put in
|
|
95
|
+
* on its own, in place of what the records suggest for the field. Answers what every field shows now.
|
|
96
|
+
*/
|
|
97
|
+
applyFill(proposals: Proposal[]): Record<string, FieldState>;
|
|
98
|
+
/** The person picked a field's offered value: what was offered from their writing has done its part. */
|
|
99
|
+
picked(field: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* The person turned down the field's suggestion. Answers whether the form must empty the field: it holds the value
|
|
102
|
+
* the form put in unasked — saved as it is, the turned-down value would count as taken.
|
|
103
|
+
*/
|
|
104
|
+
decline(field: string, values: Values): boolean;
|
|
105
|
+
/** The record was saved holding `values`: what is written next starts from them. */
|
|
106
|
+
saved(values: Values): void;
|
|
107
|
+
/** Whether the field's answer is held back now — drawn the first time a checkable value is proposed for it. */
|
|
108
|
+
private draw;
|
|
109
|
+
/** Where the suggestion comes from, in words. A field's value is shown as the record holds it now. */
|
|
110
|
+
private note;
|
|
111
|
+
}
|