@coffer-org/plugin-webchat 6.0.0 → 7.0.1
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/{ChatWidget-DFVDexpD.js → ChatWidget-DkEKl1-2.js} +406 -113
- package/dist/runtime/actions.d.ts +17 -1
- package/dist/runtime/actions.js +72 -9
- package/dist/runtime/chain-store.d.ts +20 -4
- package/dist/runtime/chain-store.js +86 -9
- package/dist/runtime/channel-registry.d.ts +9 -0
- package/dist/runtime/channel-registry.js +48 -0
- package/dist/runtime/config.js +0 -1
- package/dist/runtime/connector.d.ts +3 -2
- package/dist/runtime/connector.js +77 -42
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +6 -3
- package/dist/runtime/send.d.ts +8 -7
- package/dist/runtime/send.js +61 -29
- package/dist/schema.js +336 -21
- package/dist/web.js +1 -1
- package/locales/en.json +3 -0
- package/locales/ru.json +3 -0
- package/locales/uk.json +3 -0
- package/package.json +3 -3
package/dist/runtime/send.js
CHANGED
|
@@ -1,37 +1,52 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { handleIncoming, liveAgentId } from '@coffer-org/server/orchestrator';
|
|
3
|
-
import {
|
|
3
|
+
import { beginTurn } from '@coffer-org/server/turn-gate';
|
|
4
|
+
import { HttpError } from '@coffer-org/server/plugin-hooks';
|
|
5
|
+
import { openConversation, recordUser, buildChain, history, readSelectionForTurn, setSelection, userTurnCount, } from "./chain-store.js";
|
|
4
6
|
import { policy } from "./config.js";
|
|
5
7
|
import { makeStreamingConnector } from "./connector.js";
|
|
8
|
+
import { broadcast } from "./channel-registry.js";
|
|
6
9
|
import { webChannelSystem } from "./format.js";
|
|
7
10
|
import { loadReasoningDisplay } from "./settings.js";
|
|
11
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
12
|
+
const log = getLogger('webchat');
|
|
13
|
+
export const CAPABILITIES = {
|
|
14
|
+
events: ['delta', 'reasoning', 'segment', 'suggestions', 'title'],
|
|
15
|
+
privateChats: true,
|
|
16
|
+
};
|
|
8
17
|
export function pageContext(ctx) {
|
|
9
18
|
if (typeof ctx !== 'object' || ctx === null)
|
|
10
|
-
return
|
|
19
|
+
return [];
|
|
11
20
|
const c = ctx;
|
|
12
21
|
const path = typeof c['path'] === 'string' ? c['path'] : '';
|
|
13
22
|
if (!path)
|
|
14
|
-
return
|
|
23
|
+
return [];
|
|
15
24
|
const library = typeof c['library'] === 'string' ? c['library'] : '';
|
|
16
|
-
const
|
|
25
|
+
const shelf = typeof c['type'] === 'string' ? c['type'] : '';
|
|
17
26
|
const id = typeof c['id'] === 'string' ? c['id'] : '';
|
|
18
|
-
const
|
|
19
|
-
|
|
27
|
+
const title = typeof c['title'] === 'string' ? c['title'] : '';
|
|
28
|
+
if (library && shelf && id)
|
|
29
|
+
return [{ name: 'record', value: title, attrs: { library, shelf, id } }];
|
|
30
|
+
if (library && shelf)
|
|
31
|
+
return [{ name: 'page', value: '', attrs: { library, shelf } }];
|
|
32
|
+
return [{ name: 'page', value: path }];
|
|
20
33
|
}
|
|
21
|
-
export async function sendAction(body,
|
|
34
|
+
export async function sendAction(body, caller, deps = {}) {
|
|
22
35
|
const doHandle = deps.handleIncoming ?? handleIncoming;
|
|
23
36
|
const convId = typeof body['convId'] === 'string' ? body['convId'] : '';
|
|
24
37
|
const text = typeof body['text'] === 'string' ? body['text'] : '';
|
|
25
|
-
if (!convId)
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
if (!convId)
|
|
39
|
+
throw new HttpError(400, 'missing required field: convId');
|
|
40
|
+
if (!text.trim())
|
|
41
|
+
throw new HttpError(400, 'missing required field: text');
|
|
42
|
+
const chatId = await openConversation(convId, caller.id, 'write');
|
|
43
|
+
if (chatId === null) {
|
|
44
|
+
return { msgId: null, botMsgId: null };
|
|
28
45
|
}
|
|
29
|
-
if (!text.trim()) {
|
|
30
|
-
ctx.emit('error', { message: 'missing required field: text' });
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const chatId = chatIdFor(ctx.caller.id, convId);
|
|
34
46
|
const selection = await readSelectionForTurn(chatId);
|
|
47
|
+
if (selection.owner === null) {
|
|
48
|
+
await setSelection(chatId, { owner: caller.id });
|
|
49
|
+
}
|
|
35
50
|
const agentId = liveAgentId(selection.agentId);
|
|
36
51
|
const msgId = randomUUID();
|
|
37
52
|
const botMsgId = randomUUID();
|
|
@@ -39,27 +54,44 @@ export async function sendAction(body, ctx, deps = {}) {
|
|
|
39
54
|
const attachments = parseAttachments(body['attachments']);
|
|
40
55
|
const explicitReplyTo = typeof body['replyTo'] === 'string' && body['replyTo'] ? body['replyTo'] : null;
|
|
41
56
|
const replyTo = explicitReplyTo ?? (await history(chatId, 1)).at(-1)?.msgId ?? null;
|
|
42
|
-
await recordUser({ chatId, msgId, sender:
|
|
57
|
+
await recordUser({ chatId, msgId, sender: caller.id, text, attachments, ts: nowSec, replyToId: replyTo });
|
|
58
|
+
broadcast(chatId, 'user', { msgId, text, ts: nowSec, ...(attachments?.length ? { attachments } : {}) });
|
|
43
59
|
const messages = await buildChain(msgId, { chatId });
|
|
60
|
+
const userTurns = await userTurnCount(chatId);
|
|
44
61
|
const { connector, recorded, suggestions } = makeStreamingConnector({
|
|
45
62
|
chatId,
|
|
46
63
|
botMsgId,
|
|
47
|
-
emit: ctx.emit,
|
|
48
64
|
display: await loadReasoningDisplay(),
|
|
49
65
|
});
|
|
50
66
|
const turnContext = pageContext(body['context']);
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
const gate = await beginTurn('webchat', chatId, { supersede: true });
|
|
68
|
+
void (async () => {
|
|
69
|
+
try {
|
|
70
|
+
await doHandle(connector, {
|
|
71
|
+
envelope: { connectorId: 'webchat', chatId, turnId: msgId },
|
|
72
|
+
body: {
|
|
73
|
+
systemPrompt: { channel: [await webChannelSystem()] },
|
|
74
|
+
messages,
|
|
75
|
+
capabilities: CAPABILITIES,
|
|
76
|
+
userTurns,
|
|
77
|
+
},
|
|
78
|
+
...(turnContext.length ? { turnContext } : {}),
|
|
79
|
+
...(agentId ? { agentId } : {}),
|
|
80
|
+
...(selection.presetId ? { presetId: selection.presetId } : {}),
|
|
81
|
+
sender: { id: caller.id, idKind: 'coffer-user' },
|
|
82
|
+
signal: gate.signal,
|
|
83
|
+
}, { policy: policy() });
|
|
84
|
+
broadcast(chatId, 'done', { msgId: recorded(), parentMsgId: msgId, suggestions: suggestions() });
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
log.error(`sendAction: handleIncoming threw for chat ${chatId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
88
|
+
broadcast(chatId, 'error', { message: null, parentMsgId: msgId });
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
gate.end();
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
return { msgId, botMsgId };
|
|
63
95
|
}
|
|
64
96
|
function parseAttachments(value) {
|
|
65
97
|
if (!Array.isArray(value))
|
package/dist/schema.js
CHANGED
|
@@ -4368,6 +4368,13 @@ function wrapKey(key, opts, meta) {
|
|
|
4368
4368
|
span: opts.span
|
|
4369
4369
|
}
|
|
4370
4370
|
};
|
|
4371
|
+
if (opts.faces !== void 0) m = {
|
|
4372
|
+
...m,
|
|
4373
|
+
hints: {
|
|
4374
|
+
...m.hints,
|
|
4375
|
+
faces: opts.faces
|
|
4376
|
+
}
|
|
4377
|
+
};
|
|
4371
4378
|
if (opts.default !== void 0) m = {
|
|
4372
4379
|
...m,
|
|
4373
4380
|
default: opts.default
|
|
@@ -4795,6 +4802,7 @@ function normalizeOpts(rawIn) {
|
|
|
4795
4802
|
...v.valueLabel !== void 0 ? { valueLabel: v.valueLabel } : {},
|
|
4796
4803
|
compareWith: v.compareWith,
|
|
4797
4804
|
span: v.span,
|
|
4805
|
+
faces: v.faces,
|
|
4798
4806
|
emphasis: v.emphasis,
|
|
4799
4807
|
noLabel: v.noLabel,
|
|
4800
4808
|
role: v.role,
|
|
@@ -5107,6 +5115,63 @@ registerPreset("slug", "string", {}, {
|
|
|
5107
5115
|
};
|
|
5108
5116
|
}
|
|
5109
5117
|
});
|
|
5118
|
+
/**
|
|
5119
|
+
* Identifier — a code printed for a machine and read back by one: a serial number, an order
|
|
5120
|
+
* number, a policy number, an IMEI. What a person actually DOES with one is copy it, which is
|
|
5121
|
+
* why this is a type of its own and not `f.string({ ui: { voice: 'data' } })`: the machine face
|
|
5122
|
+
* is one word on any string now, but the click that copies belongs to the kind.
|
|
5123
|
+
*
|
|
5124
|
+
* Deliberately unvalidated beyond a length: an identifier's format belongs to whoever issued
|
|
5125
|
+
* it, and a pattern here would reject the next manufacturer's.
|
|
5126
|
+
*
|
|
5127
|
+
* @layer preset
|
|
5128
|
+
* @base string
|
|
5129
|
+
* @prim text
|
|
5130
|
+
* @widget identifier
|
|
5131
|
+
* @example f.identifier({ label: 'mod.fields.serialNumber' })
|
|
5132
|
+
*/
|
|
5133
|
+
function identifier(o) {
|
|
5134
|
+
return declare("identifier", o);
|
|
5135
|
+
}
|
|
5136
|
+
registerPreset("identifier", "string", {}, {
|
|
5137
|
+
kind: "identifier",
|
|
5138
|
+
widget: "identifier",
|
|
5139
|
+
build() {
|
|
5140
|
+
return {
|
|
5141
|
+
column: "text",
|
|
5142
|
+
zod: string$1(),
|
|
5143
|
+
hints: {}
|
|
5144
|
+
};
|
|
5145
|
+
}
|
|
5146
|
+
});
|
|
5147
|
+
/**
|
|
5148
|
+
* The machine-readable zone of a travel document — the identifier taken to its limit: printed
|
|
5149
|
+
* for a scanner, not for a person. Fixed pitch, chevrons kept as the filler they are, and the
|
|
5150
|
+
* line breaks preserved, because an MRZ's line structure is part of what it encodes.
|
|
5151
|
+
*
|
|
5152
|
+
* Unvalidated on purpose, like `identifier`: the ICAO line formats differ by document type, and
|
|
5153
|
+
* a strip transcribed from a real document is worth storing even when it does not check out.
|
|
5154
|
+
*
|
|
5155
|
+
* @layer preset
|
|
5156
|
+
* @base string
|
|
5157
|
+
* @prim text
|
|
5158
|
+
* @widget mrz
|
|
5159
|
+
* @example f.mrz({ label: 'documents.personal_document.fields.mrz' })
|
|
5160
|
+
*/
|
|
5161
|
+
function mrz(o) {
|
|
5162
|
+
return declare("mrz", o);
|
|
5163
|
+
}
|
|
5164
|
+
registerPreset("mrz", "string", {}, {
|
|
5165
|
+
kind: "mrz",
|
|
5166
|
+
widget: "mrz",
|
|
5167
|
+
build() {
|
|
5168
|
+
return {
|
|
5169
|
+
column: "text",
|
|
5170
|
+
zod: string$1(),
|
|
5171
|
+
hints: {}
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5174
|
+
});
|
|
5110
5175
|
var COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
5111
5176
|
/**
|
|
5112
5177
|
* Hex color — kind 'color', `#rrggbb` + swatch widget.
|
|
@@ -5391,6 +5456,38 @@ registerPreset("tag", "string", {}, {
|
|
|
5391
5456
|
}
|
|
5392
5457
|
});
|
|
5393
5458
|
/**
|
|
5459
|
+
* A location written the way it is said aloud: workshop › shelf 2 › box. A `string` fixed to
|
|
5460
|
+
* `multiple`, exactly as `f.tags` is, because a place inside a place inside a place IS a
|
|
5461
|
+
* sequence — the plurality is the type, not a modifier someone remembered to add.
|
|
5462
|
+
*
|
|
5463
|
+
* Distinct from `f.tags`, which stores the same shape: tags are an unordered SET, and a path is
|
|
5464
|
+
* an ordered CHAIN where each step is inside the one before it. Drawn as chips, that
|
|
5465
|
+
* containment — the only thing the value actually carries — is lost.
|
|
5466
|
+
*
|
|
5467
|
+
* @layer preset
|
|
5468
|
+
* @base string
|
|
5469
|
+
* @prim text
|
|
5470
|
+
* @widget path
|
|
5471
|
+
* @example f.path({ label: 'things.storage_location.fields.path' })
|
|
5472
|
+
*/
|
|
5473
|
+
function path(o) {
|
|
5474
|
+
return declare("path", {
|
|
5475
|
+
...o,
|
|
5476
|
+
multiple: true
|
|
5477
|
+
});
|
|
5478
|
+
}
|
|
5479
|
+
registerPreset("path", "string", {}, {
|
|
5480
|
+
kind: "path",
|
|
5481
|
+
widget: "path",
|
|
5482
|
+
build() {
|
|
5483
|
+
return {
|
|
5484
|
+
column: "text",
|
|
5485
|
+
zod: string$1(),
|
|
5486
|
+
hints: {}
|
|
5487
|
+
};
|
|
5488
|
+
}
|
|
5489
|
+
});
|
|
5490
|
+
/**
|
|
5394
5491
|
* Tags — `tag({ multiple: true })`.
|
|
5395
5492
|
*
|
|
5396
5493
|
* @layer preset
|
|
@@ -5583,6 +5680,56 @@ registerPreset("reminder", "date", { lead: 30 }, {
|
|
|
5583
5680
|
};
|
|
5584
5681
|
}
|
|
5585
5682
|
});
|
|
5683
|
+
/** Timetable — a set of times of day, read as a schedule rather than as a bag of values.
|
|
5684
|
+
* kind 'timetable', prim 'time', always `multiple`.
|
|
5685
|
+
*
|
|
5686
|
+
* Same shape as `f.reminder`/`f.age` over `date`: a semantic type whose renderer knows what
|
|
5687
|
+
* the values MEAN. Everything it shows beyond the times themselves — when the service starts
|
|
5688
|
+
* and ends, how many runs there are, which one is next — is derived from the values at render
|
|
5689
|
+
* time, never stored: a "next departure" is wrong the moment it is written down, exactly the
|
|
5690
|
+
* argument `f.age` already makes for an age.
|
|
5691
|
+
*
|
|
5692
|
+
* Storage is a plain multiple `time`, so a field can be switched to this from
|
|
5693
|
+
* `f.time({ multiple: true })` and back with no migration.
|
|
5694
|
+
*
|
|
5695
|
+
* @layer preset
|
|
5696
|
+
* @base time
|
|
5697
|
+
* @prim time
|
|
5698
|
+
* @widget timetable
|
|
5699
|
+
* @example f.timetable({ label: 'mod.fields.weekdays' })
|
|
5700
|
+
*/
|
|
5701
|
+
function timetable(o) {
|
|
5702
|
+
return declare("timetable", {
|
|
5703
|
+
...o,
|
|
5704
|
+
multiple: true
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
registerPreset("timetable", "time", {}, {
|
|
5708
|
+
kind: "timetable",
|
|
5709
|
+
widget: "timetable"
|
|
5710
|
+
});
|
|
5711
|
+
/** Age — date whose whole-year age (as of today) renders alongside it, e.g. a birth date.
|
|
5712
|
+
* kind 'age', prim 'date'. The age itself is computed at RENDER time, never stored: unlike a
|
|
5713
|
+
* `mutate`-driven computed field, it must be right on every day that passes, not only the day
|
|
5714
|
+
* the record was last written, so this is display metadata over `date`, the same shape as
|
|
5715
|
+
* `f.reminder`'s own relative-to-today note.
|
|
5716
|
+
*
|
|
5717
|
+
* @layer preset
|
|
5718
|
+
* @base date
|
|
5719
|
+
* @prim date
|
|
5720
|
+
* @widget age
|
|
5721
|
+
* @example f.age({ label: 'mod.fields.birthDate' })
|
|
5722
|
+
*/
|
|
5723
|
+
function age(o) {
|
|
5724
|
+
return declare("age", o);
|
|
5725
|
+
}
|
|
5726
|
+
registerPreset("age", "date", {}, {
|
|
5727
|
+
kind: "age",
|
|
5728
|
+
widget: "age",
|
|
5729
|
+
build(o, parts) {
|
|
5730
|
+
return typeOf("date").build(o, parts);
|
|
5731
|
+
}
|
|
5732
|
+
});
|
|
5586
5733
|
/**
|
|
5587
5734
|
* Percentage 0..100 — real with rules:{min:0,max:100}.
|
|
5588
5735
|
*
|
|
@@ -5775,6 +5922,9 @@ var presets = {
|
|
|
5775
5922
|
email,
|
|
5776
5923
|
tel,
|
|
5777
5924
|
slug,
|
|
5925
|
+
path,
|
|
5926
|
+
identifier,
|
|
5927
|
+
mrz,
|
|
5778
5928
|
color,
|
|
5779
5929
|
colorname,
|
|
5780
5930
|
title,
|
|
@@ -5788,6 +5938,8 @@ var presets = {
|
|
|
5788
5938
|
rating,
|
|
5789
5939
|
duration,
|
|
5790
5940
|
reminder,
|
|
5941
|
+
age,
|
|
5942
|
+
timetable,
|
|
5791
5943
|
percent,
|
|
5792
5944
|
year,
|
|
5793
5945
|
weight,
|
|
@@ -6445,26 +6597,38 @@ function identity(opts) {
|
|
|
6445
6597
|
}
|
|
6446
6598
|
};
|
|
6447
6599
|
}
|
|
6448
|
-
/**
|
|
6600
|
+
/** One measure out of an optional max, from ONE OR MORE named sources, with an optional verdict.
|
|
6601
|
+
*
|
|
6602
|
+
* `source` takes several entries for the same reason `f.identity`'s `channels` does — it is a
|
|
6603
|
+
* role that holds a LIST, so `view.source` is an ordered list of names rather than one name.
|
|
6604
|
+
* Two ratings of the same film out of ten are one measure read twice, and drawing them apart is
|
|
6605
|
+
* what makes them incomparable: the eye has to carry the scale between two figures instead of
|
|
6606
|
+
* reading them against a shared one. `media/title` had exactly that, two `f.score` blocks of
|
|
6607
|
+
* `max: 10` held apart inside an `f.compare`, until this took the restriction off.
|
|
6608
|
+
*
|
|
6609
|
+
* NOT `multiple`. A multiple field is an anonymous array; these are NAMED sources, and the name
|
|
6610
|
+
* is what says which reading came from where. The two are different shapes and the block wants
|
|
6611
|
+
* this one.
|
|
6449
6612
|
*
|
|
6450
6613
|
* @layer block
|
|
6451
6614
|
* @base group
|
|
6452
6615
|
* @prim —
|
|
6453
6616
|
* @widget score
|
|
6454
|
-
* @example f.score({ source: {
|
|
6617
|
+
* @example f.score({ source: { imdb: f.real({ label: '…' }), tmdb: f.real({ label: '…' }) }, max: 10 })
|
|
6455
6618
|
*/
|
|
6456
6619
|
function score(opts) {
|
|
6457
|
-
const
|
|
6620
|
+
const names = Object.keys(opts.source ?? {});
|
|
6621
|
+
if (names.length === 0) throw new Error(`[field.score] slot 'source' expects at least one entry, got 0`);
|
|
6458
6622
|
const verdict = opts.verdict !== void 0 ? slot("score", "verdict", opts.verdict) : void 0;
|
|
6459
6623
|
return {
|
|
6460
6624
|
...group({
|
|
6461
6625
|
label: opts.label,
|
|
6462
|
-
fields: merge("score", source, verdict),
|
|
6626
|
+
fields: merge("score", opts.source, verdict),
|
|
6463
6627
|
ui: { kind: "score" }
|
|
6464
6628
|
}),
|
|
6465
6629
|
view: {
|
|
6466
6630
|
kind: "score",
|
|
6467
|
-
source:
|
|
6631
|
+
source: names,
|
|
6468
6632
|
max: opts.max,
|
|
6469
6633
|
verdict: verdict && Object.keys(verdict)[0]
|
|
6470
6634
|
}
|
|
@@ -6496,11 +6660,20 @@ function status(opts) {
|
|
|
6496
6660
|
}
|
|
6497
6661
|
/** A gauge value between an optional min and max, with an optional "of" total.
|
|
6498
6662
|
*
|
|
6663
|
+
* `direction` is opt-in: when given, the gauge decides in plain JavaScript whether its own
|
|
6664
|
+
* value crossed the bound that matters (`meterTone`, `blocks/meter.tsx`) and reaches for the
|
|
6665
|
+
* tone palette — the fill, the value text and the `of` companion all move together, never
|
|
6666
|
+
* colour alone (a glyph rides along, see the renderer's own comment). Omit it and nothing
|
|
6667
|
+
* about the gauge changes from today. A `'ceiling'` gauge with no declared `max` (config or
|
|
6668
|
+
* the source field's own hints) resolves it from `of`'s own value instead — a budget's cap is
|
|
6669
|
+
* a per-record field, never a compile-time constant.
|
|
6670
|
+
*
|
|
6499
6671
|
* @layer block
|
|
6500
6672
|
* @base group
|
|
6501
6673
|
* @prim —
|
|
6502
6674
|
* @widget meter
|
|
6503
6675
|
* @example f.meter({ source: { used: f.real({ label: '…' }) }, min: 0, max: 100 })
|
|
6676
|
+
* @example f.meter({ source: { spent: f.real({ label: '…' }) }, of: { budget: f.real({ label: '…' }) }, direction: 'ceiling' })
|
|
6504
6677
|
*/
|
|
6505
6678
|
function meter(opts) {
|
|
6506
6679
|
const source = slot("meter", "source", opts.source);
|
|
@@ -6516,7 +6689,8 @@ function meter(opts) {
|
|
|
6516
6689
|
source: Object.keys(source)[0],
|
|
6517
6690
|
min: opts.min,
|
|
6518
6691
|
max: opts.max,
|
|
6519
|
-
of: of && Object.keys(of)[0]
|
|
6692
|
+
of: of && Object.keys(of)[0],
|
|
6693
|
+
direction: opts.direction
|
|
6520
6694
|
}
|
|
6521
6695
|
};
|
|
6522
6696
|
}
|
|
@@ -6576,6 +6750,11 @@ function balance(opts) {
|
|
|
6576
6750
|
* @base group
|
|
6577
6751
|
* @prim —
|
|
6578
6752
|
* @widget route
|
|
6753
|
+
* A `stub` is the part of the ticket that is TORN OFF and kept — a seat, a gate, a booking
|
|
6754
|
+
* reference. Declaring one makes the block a ticket rather than a line: the two halves are
|
|
6755
|
+
* separated by a perforation, and the fields in the stub sit below it. Without one the block
|
|
6756
|
+
* is exactly what it was, a route from here to there, so no existing call changes.
|
|
6757
|
+
*
|
|
6579
6758
|
* @example f.route({ from: { from: f.string({ label: '…' }) }, to: { to: f.string({ label: '…' }) } })
|
|
6580
6759
|
*/
|
|
6581
6760
|
function route(opts) {
|
|
@@ -6584,9 +6763,10 @@ function route(opts) {
|
|
|
6584
6763
|
const depart = opts.depart !== void 0 ? slot("route", "depart", opts.depart) : void 0;
|
|
6585
6764
|
const arrive = opts.arrive !== void 0 ? slot("route", "arrive", opts.arrive) : void 0;
|
|
6586
6765
|
const duration = opts.duration !== void 0 ? slot("route", "duration", opts.duration) : void 0;
|
|
6766
|
+
const stub = opts.stub ?? {};
|
|
6587
6767
|
return {
|
|
6588
6768
|
...group({
|
|
6589
|
-
fields: merge("route", from, to, depart, arrive, duration),
|
|
6769
|
+
fields: merge("route", from, to, depart, arrive, duration, stub),
|
|
6590
6770
|
ui: { kind: "route" }
|
|
6591
6771
|
}),
|
|
6592
6772
|
view: {
|
|
@@ -6595,7 +6775,8 @@ function route(opts) {
|
|
|
6595
6775
|
to: Object.keys(to)[0],
|
|
6596
6776
|
depart: depart && Object.keys(depart)[0],
|
|
6597
6777
|
arrive: arrive && Object.keys(arrive)[0],
|
|
6598
|
-
duration: duration && Object.keys(duration)[0]
|
|
6778
|
+
duration: duration && Object.keys(duration)[0],
|
|
6779
|
+
stub: Object.keys(stub)
|
|
6599
6780
|
}
|
|
6600
6781
|
};
|
|
6601
6782
|
}
|
|
@@ -6773,6 +6954,81 @@ function specimen(opts) {
|
|
|
6773
6954
|
};
|
|
6774
6955
|
}
|
|
6775
6956
|
/**
|
|
6957
|
+
* Measurements against the range they were supposed to fall in, rendered from a single
|
|
6958
|
+
* collection source — see `stampCollection`. `analyte`, `value`, `unit`, `low` and `high` are
|
|
6959
|
+
* part KEYS inside each collection row, NOT `LayoutEl` positions — the same reasoning as
|
|
6960
|
+
* `manifest`'s `quantity`/`item`.
|
|
6961
|
+
*
|
|
6962
|
+
* The point is the COMPARISON. A `f.table` over the same rows prints the bounds as two more
|
|
6963
|
+
* columns and leaves the reader to do it; here each value sits on its own band with the
|
|
6964
|
+
* reference span marked on it, so "outside the range" is seen rather than worked out. That is
|
|
6965
|
+
* also the only thing on the row worth a colour — it is what the reader has to act on.
|
|
6966
|
+
*
|
|
6967
|
+
* `unit`, `low` and `high` are optional: a measurement with no published range (a culture, a
|
|
6968
|
+
* description) still belongs in the same list and simply gets no band.
|
|
6969
|
+
*
|
|
6970
|
+
* @layer block
|
|
6971
|
+
* @base group
|
|
6972
|
+
* @prim —
|
|
6973
|
+
* @widget assay
|
|
6974
|
+
* @example f.assay({ source: { results: f.group({ scope: 'nest', multiple: true, fields: { analyte: f.string({ label: '…' }), value: f.real({ label: '…' }) } }) }, analyte: 'analyte', value: 'value' })
|
|
6975
|
+
*/
|
|
6976
|
+
function assay(opts) {
|
|
6977
|
+
return stampCollection("assay", opts.source, opts.label, {
|
|
6978
|
+
kind: "assay",
|
|
6979
|
+
analyte: opts.analyte,
|
|
6980
|
+
value: opts.value,
|
|
6981
|
+
...opts.unit ? { unit: opts.unit } : {},
|
|
6982
|
+
...opts.low ? { low: opts.low } : {},
|
|
6983
|
+
...opts.high ? { high: opts.high } : {}
|
|
6984
|
+
});
|
|
6985
|
+
}
|
|
6986
|
+
/**
|
|
6987
|
+
* Value moving from one place to another, where the MOVEMENT is the subject — not one more
|
|
6988
|
+
* labelled row among the record's fields. The sum is set large in the machine voice, and the two
|
|
6989
|
+
* ends read as a path beneath it.
|
|
6990
|
+
*
|
|
6991
|
+
* amount — what moved. The one required role, and the reason the block exists
|
|
6992
|
+
* from — where it left. Absent on money that only arrived
|
|
6993
|
+
* to — where it arrived. Absent on money that only left
|
|
6994
|
+
* meta — the record's own remaining fields, under a rule: date, category, reference
|
|
6995
|
+
*
|
|
6996
|
+
* An absent end is not drawn, and that is the whole reading: money that left an account and
|
|
6997
|
+
* arrived nowhere IS an expense. It is read off which ends the record actually has, never
|
|
6998
|
+
* guessed from the shape of a value. What the movement MEANS beyond that — whether this
|
|
6999
|
+
* particular kind of transfer is good news — is carried by the classifier's own option `tone`,
|
|
7000
|
+
* the same declaration every other coloured value in the product uses.
|
|
7001
|
+
*
|
|
7002
|
+
* Distinct from `f.route`, which also has two ends: there the journey is the subject and the
|
|
7003
|
+
* ends are places, so it carries times and a duration; here the ends are accounts and the
|
|
7004
|
+
* subject is the quantity.
|
|
7005
|
+
*
|
|
7006
|
+
* @layer block
|
|
7007
|
+
* @base group
|
|
7008
|
+
* @prim —
|
|
7009
|
+
* @widget flow
|
|
7010
|
+
* @example f.flow({ amount: { amount: f.money({ label: '…' }) }, from: { source: f.relation({ label: '…' }) } })
|
|
7011
|
+
*/
|
|
7012
|
+
function flow(opts) {
|
|
7013
|
+
const amount = slot("flow", "amount", opts.amount);
|
|
7014
|
+
const from = opts.from !== void 0 ? slot("flow", "from", opts.from) : void 0;
|
|
7015
|
+
const to = opts.to !== void 0 ? slot("flow", "to", opts.to) : void 0;
|
|
7016
|
+
return {
|
|
7017
|
+
...group({
|
|
7018
|
+
label: opts.label,
|
|
7019
|
+
fields: merge("flow", amount, from, to, opts.meta),
|
|
7020
|
+
ui: { kind: "flow" }
|
|
7021
|
+
}),
|
|
7022
|
+
view: {
|
|
7023
|
+
kind: "flow",
|
|
7024
|
+
amount: Object.keys(amount)[0],
|
|
7025
|
+
from: from && Object.keys(from)[0],
|
|
7026
|
+
to: to && Object.keys(to)[0],
|
|
7027
|
+
meta: Object.keys(opts.meta ?? {})
|
|
7028
|
+
}
|
|
7029
|
+
};
|
|
7030
|
+
}
|
|
7031
|
+
/**
|
|
6776
7032
|
* A packing/cargo manifest rendered from a single collection source — see `stampCollection`.
|
|
6777
7033
|
* `quantity` and `item` are part KEYS inside each collection row, NOT `LayoutEl` positions —
|
|
6778
7034
|
* the same reasoning as `journal`'s `date`/`text`.
|
|
@@ -6819,13 +7075,25 @@ function table(opts) {
|
|
|
6819
7075
|
} : {},
|
|
6820
7076
|
...opts.groupBy ? { groupBy: opts.groupBy } : {},
|
|
6821
7077
|
...opts.totals ? { totals: opts.totals } : {},
|
|
7078
|
+
...opts.summary ? { summary: opts.summary } : {},
|
|
6822
7079
|
...opts.numbered ? { numbered: true } : {}
|
|
6823
7080
|
});
|
|
6824
7081
|
}
|
|
6825
7082
|
/**
|
|
6826
|
-
* An
|
|
6827
|
-
*
|
|
6828
|
-
*
|
|
7083
|
+
* An identity document, drawn as the card it is: a caption and its number across the top, a
|
|
7084
|
+
* portrait beside the holder's own fields, the issuing details under a rule, and the
|
|
7085
|
+
* machine-readable strip at the foot. `view` names each role (Task 5) — see `masthead`'s own
|
|
7086
|
+
* doc comment for what an ordered name list means.
|
|
7087
|
+
*
|
|
7088
|
+
* overline — the caption at the top left ("PASSPORT", "DRIVING LICENCE")
|
|
7089
|
+
* number — the document's number, set apart at the top right
|
|
7090
|
+
* photo — the portrait. One field, rendered in a portrait frame beside the body
|
|
7091
|
+
* meta — the card's OWN fields, labelled, at each field's declared span
|
|
7092
|
+
* footer — the fields below the rule: issued, authority, record number
|
|
7093
|
+
* mrz — the machine-readable strip, set in monospace at the foot
|
|
7094
|
+
*
|
|
7095
|
+
* Every role is optional but `number`: a bank card has no `mrz`, a library card no `photo`,
|
|
7096
|
+
* and the card simply omits the part it was given nothing for.
|
|
6829
7097
|
*
|
|
6830
7098
|
* @layer block
|
|
6831
7099
|
* @base group
|
|
@@ -6836,16 +7104,21 @@ function table(opts) {
|
|
|
6836
7104
|
function idcard(opts) {
|
|
6837
7105
|
const overline = opts.overline !== void 0 ? slot("idcard", "overline", opts.overline) : void 0;
|
|
6838
7106
|
const number = slot("idcard", "number", opts.number);
|
|
7107
|
+
const photo = opts.photo !== void 0 ? slot("idcard", "photo", opts.photo) : void 0;
|
|
7108
|
+
const mrz = opts.mrz !== void 0 ? slot("idcard", "mrz", opts.mrz) : void 0;
|
|
6839
7109
|
return {
|
|
6840
7110
|
...group({
|
|
6841
|
-
fields: merge("idcard", overline, number, opts.meta),
|
|
7111
|
+
fields: merge("idcard", overline, number, photo, opts.meta, opts.footer, mrz),
|
|
6842
7112
|
ui: { kind: "idcard" }
|
|
6843
7113
|
}),
|
|
6844
7114
|
view: {
|
|
6845
7115
|
kind: "idcard",
|
|
6846
7116
|
overline: overline && Object.keys(overline)[0],
|
|
6847
7117
|
number: Object.keys(number)[0],
|
|
6848
|
-
|
|
7118
|
+
photo: photo && Object.keys(photo)[0],
|
|
7119
|
+
meta: Object.keys(opts.meta ?? {}),
|
|
7120
|
+
footer: Object.keys(opts.footer ?? {}),
|
|
7121
|
+
mrz: mrz && Object.keys(mrz)[0]
|
|
6849
7122
|
}
|
|
6850
7123
|
};
|
|
6851
7124
|
}
|
|
@@ -6930,6 +7203,8 @@ var blocks = {
|
|
|
6930
7203
|
nutrition,
|
|
6931
7204
|
specimen,
|
|
6932
7205
|
manifest,
|
|
7206
|
+
assay,
|
|
7207
|
+
flow,
|
|
6933
7208
|
table,
|
|
6934
7209
|
idcard,
|
|
6935
7210
|
properties,
|
|
@@ -7777,6 +8052,11 @@ function group(o) {
|
|
|
7777
8052
|
for (const f of fields) if (!("key" in f && f.key !== void 0) && !hasChildren(f)) throw new Error(`[field.group] a 'nest' group's children must be fields or groups, not layout elements`);
|
|
7778
8053
|
for (const k of r.unique ?? []) if (!fields.some((f) => "key" in f && f.key === k)) throw new Error(`[field.group] unique key '${k}' is not a subfield`);
|
|
7779
8054
|
} else if (o.value !== void 0) throw new Error(`[field.group] value requires scope 'nest' — a hoist group owns no children to write`);
|
|
8055
|
+
const title = v.title ?? [];
|
|
8056
|
+
if (title.length > 0) {
|
|
8057
|
+
if (!o.multiple) throw new Error(`[field.group] ui.title heads a collection's ROWS — it needs multiple: true`);
|
|
8058
|
+
for (const name of title) if (!fields.some((f) => "key" in f && f.key === name)) throw new Error(`[field.group] ui.title '${name}' is not one of its fields`);
|
|
8059
|
+
}
|
|
7780
8060
|
return {
|
|
7781
8061
|
el: "group",
|
|
7782
8062
|
scope,
|
|
@@ -7789,6 +8069,7 @@ function group(o) {
|
|
|
7789
8069
|
display: v.display ?? "wrap",
|
|
7790
8070
|
kind: v.kind,
|
|
7791
8071
|
fixed: r.fixed,
|
|
8072
|
+
view: title.length > 0 ? { title } : void 0,
|
|
7792
8073
|
fields
|
|
7793
8074
|
};
|
|
7794
8075
|
}
|
|
@@ -7937,6 +8218,12 @@ function divider(o) {
|
|
|
7937
8218
|
/**
|
|
7938
8219
|
* Reference markdown block by i18n key.
|
|
7939
8220
|
*
|
|
8221
|
+
* The key is resolved WITH the variables of the running instance, so the locale string
|
|
8222
|
+
* may name facts that are not constants: `{{origin}}` is this instance's own address
|
|
8223
|
+
* (`https://coffer.example`), which is how a block documents an endpoint without
|
|
8224
|
+
* freezing a developer's `localhost` into every translation. The set is fixed and
|
|
8225
|
+
* ambient — the block declares only its key. See `web-ui/src/render/info-text.ts`.
|
|
8226
|
+
*
|
|
7940
8227
|
* @layer primitive
|
|
7941
8228
|
* @prim —
|
|
7942
8229
|
* @widget info
|
|
@@ -7949,6 +8236,31 @@ function info(textKey) {
|
|
|
7949
8236
|
};
|
|
7950
8237
|
}
|
|
7951
8238
|
/**
|
|
8239
|
+
* What else points AT this record — the inverse of a relation, which is often the more useful
|
|
8240
|
+
* direction on a record page: a box is more usefully "what is in it" than "what it is in".
|
|
8241
|
+
*
|
|
8242
|
+
* Stores NOTHING and has no column: it declares where to look, and the server answers by asking
|
|
8243
|
+
* the pointing shelf. That is why it is a pseudo-element beside `divider` and `info` rather than
|
|
8244
|
+
* a field — there is no value here to validate or save.
|
|
8245
|
+
*
|
|
8246
|
+
* `from` names the pointing side explicitly (which shelf, and WHICH of its fields), never
|
|
8247
|
+
* "everything that happens to point here": a shelf may point at the same target through two
|
|
8248
|
+
* fields — a transaction has a source account and a destination account — and a panel that
|
|
8249
|
+
* merged them would answer a question nobody asked.
|
|
8250
|
+
*
|
|
8251
|
+
* @layer primitive
|
|
8252
|
+
* @prim —
|
|
8253
|
+
* @widget backrefs
|
|
8254
|
+
* @example f.backrefs({ label: 'mod.fields.storedHere', from: { library: 'things', shelf: 'item', field: 'location' } })
|
|
8255
|
+
*/
|
|
8256
|
+
function backrefs(o) {
|
|
8257
|
+
return {
|
|
8258
|
+
el: "backrefs",
|
|
8259
|
+
label: o.label,
|
|
8260
|
+
from: o.from
|
|
8261
|
+
};
|
|
8262
|
+
}
|
|
8263
|
+
/**
|
|
7952
8264
|
* Action button: invokes the handler registered in actionRegistry under the key `value`.
|
|
7953
8265
|
*
|
|
7954
8266
|
* @layer primitive
|
|
@@ -8273,20 +8585,22 @@ registerType("time", {
|
|
|
8273
8585
|
prim: "time",
|
|
8274
8586
|
widget: "time",
|
|
8275
8587
|
build(o) {
|
|
8276
|
-
const granularity = o.config?.granularity ?? "
|
|
8588
|
+
const granularity = o.config?.granularity ?? "minute";
|
|
8589
|
+
const s = string$1(reqErr()).regex({
|
|
8590
|
+
hour: /^([01]\d|2[0-3])$/,
|
|
8591
|
+
minute: /^([01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/,
|
|
8592
|
+
second: /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/
|
|
8593
|
+
}[granularity], { message: vmsg("time_format") });
|
|
8277
8594
|
return {
|
|
8278
8595
|
column: "time",
|
|
8279
|
-
zod:
|
|
8280
|
-
hour: /^([01]\d|2[0-3])$/,
|
|
8281
|
-
minute: /^([01]\d|2[0-3]):[0-5]\d$/,
|
|
8282
|
-
second: /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/
|
|
8283
|
-
}[granularity], { message: vmsg("time_format") }),
|
|
8596
|
+
zod: granularity === "minute" ? s.transform((v) => v.slice(0, 5)) : s,
|
|
8284
8597
|
hints: { granularity }
|
|
8285
8598
|
};
|
|
8286
8599
|
}
|
|
8287
8600
|
});
|
|
8288
8601
|
/**
|
|
8289
|
-
* Time-of-day value with configurable granularity ('hour' | 'minute' | 'second', default
|
|
8602
|
+
* Time-of-day value with configurable granularity ('hour' | 'minute' | 'second', default
|
|
8603
|
+
* 'minute' — the same default `f.datetime` has always had).
|
|
8290
8604
|
*
|
|
8291
8605
|
* @layer primitive
|
|
8292
8606
|
* @prim time
|
|
@@ -9531,7 +9845,8 @@ var PRIMITIVES = {
|
|
|
9531
9845
|
url,
|
|
9532
9846
|
divider,
|
|
9533
9847
|
info,
|
|
9534
|
-
button
|
|
9848
|
+
button,
|
|
9849
|
+
backrefs
|
|
9535
9850
|
};
|
|
9536
9851
|
/** Assembles `f`, guaranteeing no preset/block shadows a primitive. */
|
|
9537
9852
|
function composeF(presets, blocks) {
|