@lotics/ui 15.2.0 → 16.0.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/AGENTS.md +1 -1
- package/MIGRATION.md +13 -0
- package/docs/ai_patterns.md +41 -24
- package/docs/catalog.md +16 -1
- package/examples/tpl_item_list.tsx +71 -25
- package/examples/tpl_lookup.tsx +11 -6
- package/examples/tpl_task_board.tsx +1 -2
- package/package.json +2 -1
- package/src/confidence.tsx +46 -30
- package/src/result_header.tsx +79 -0
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
16
16
|
|---|---|
|
|
17
17
|
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` for a custom-code app's `vite.config.ts`). Read before building any screen; reuse first. |
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (incl. the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates. |
|
|
19
|
-
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split (modify → review-before-apply; create → save-direct +
|
|
19
|
+
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split (modify → review-before-apply; create → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md). |
|
|
20
20
|
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude, banded cards, register vs inset rows, master-detail `Drawer`, view controls, color discipline, typography, whitespace. |
|
|
21
21
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget). |
|
|
22
22
|
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,19 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## v16 from 15.x
|
|
8
|
+
|
|
9
|
+
**`Confidence` IS now a callout with a REQUIRED basis** — `children` (what was checked and
|
|
10
|
+
what came of it) is mandatory, the meter + level phrase head the block, and the box stays
|
|
11
|
+
white with a hairline border at every level (the meter is the only colored element). The
|
|
12
|
+
standalone three-tick meter no longer exists: a bare `<Confidence level="high" />` fails to type-check. Migrate by
|
|
13
|
+
adding the basis (`<Confidence level="low">2 of 7 references fail their checksum (X, Y) —
|
|
14
|
+
verify against the source.</Confidence>`) — or, where a compact meter decorated a ranked row
|
|
15
|
+
or a proposal card, remove it: rank, badges, and severity already convey standing there.
|
|
16
|
+
|
|
17
|
+
Additively in v16: `ResultHeader` (`@lotics/ui/result_header`) — the save-direct receipt's
|
|
18
|
+
outcome strip; see ai_patterns §the one law for the receipt composition.
|
|
19
|
+
|
|
7
20
|
## v15 from 14.x
|
|
8
21
|
|
|
9
22
|
`AgentRun`/`AgentProgress`'s **`labelForTool`** prop was replaced by **`labelForCall`** — the
|
package/docs/ai_patterns.md
CHANGED
|
@@ -18,19 +18,31 @@ the human stays in charge follows the stakes — split by whether a before-state
|
|
|
18
18
|
- **Modifying an existing record** — a diff exists, and a wrong value can clobber a right one →
|
|
19
19
|
review BEFORE apply: the `ChangeReview` family, Keep/Drop per change, nothing auto-applies.
|
|
20
20
|
- **Creating new records** (intake, import, drafting rows) — there is no before-state, so there
|
|
21
|
-
is nothing to diff → **apply directly, then
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
is nothing to diff → **apply directly, then end on the RECEIPT**
|
|
22
|
+
(`@lotics/ui/result_header`) — the receipt states the outcome and ROUTES; it is never a
|
|
23
|
+
record editor. Run DETERMINISTIC checks over what the save STORED (a checksum, a count
|
|
24
|
+
reconciliation — binary math, never a fabricated score). ONE record: `ResultHeader` (tone ·
|
|
25
|
+
outcome title) → the RECEIPT LINES — a `DetailTable` of spread `DetailRow`s (who · how
|
|
26
|
+
much · how many · the schedule) — → **`Confidence`**: the level WITH its basis as
|
|
27
|
+
one block, always (the happy path says what was checked; the flagged path NAMES the exact
|
|
28
|
+
failing values — the user knows what to verify BEFORE opening). Confidence never floats
|
|
29
|
+
alone as a bare meter on a result surface. NOTHING in the receipt edits: the RECORD is the edit surface (its
|
|
30
|
+
flagged cells carry the same amber marks), one press away — correction uses the record's
|
|
31
|
+
ordinary verbs. SEVERAL records: a compact attention-first RESULT REGISTER (`ListItem` per
|
|
32
|
+
record — tone mark · outcome title · key figures · the needs-checking count) whose rows
|
|
33
|
+
press STRAIGHT THROUGH to the record — no intermediate screen. Never tabs (they cap out),
|
|
34
|
+
never stacked collapsed cards, never a field wall duplicating the record page. A
|
|
35
|
+
flagged value names its exact verdict in amber (a reason line under the value, or a check
|
|
36
|
+
column cell) and is corrected IN PLACE — `InlineTextInput.onSave` commits immediately
|
|
37
|
+
through the ordinary update workflow (the record is saved; nothing stages, no apply button
|
|
38
|
+
exists). A batch-level failure lands on EVERY record as its reason — never ALSO a
|
|
39
|
+
banner above the list (a top `Callout` is only for information the records don't carry).
|
|
40
|
+
The closing footer button is dismissal (secondary "Done"), never primary. Correction beyond
|
|
41
|
+
the receipt uses the record's ORDINARY verbs — open it, edit or delete (make the delete
|
|
42
|
+
workflow CASCADE the record's children, so deletion is always safe) — never a special undo
|
|
43
|
+
affordance duplicating the delete verb. A review GATE on a create degenerates into
|
|
44
|
+
proofreading a wall of cards the user blind-confirms — and a human cannot eyeball-verify a
|
|
45
|
+
reference number anyway; deterministic post-save checks catch what proofreading cannot.
|
|
34
46
|
|
|
35
47
|
Either way the deterministic app applies the write. Compose the surfaces as a loop, and reach
|
|
36
48
|
for the right one by job.
|
|
@@ -42,7 +54,8 @@ Three shapes:
|
|
|
42
54
|
1. **Field writes** (extract, match, rank, classify → records change) → **in-app agent**
|
|
43
55
|
(`useAgentRun`), then split by the one law: modifying an existing record → **`ChangeReview`**
|
|
44
56
|
(Keep/Drop per change, app-workflow writes, bounded app authority); creating new records →
|
|
45
|
-
**save direct +
|
|
57
|
+
**save direct + the receipt** (outcome, verdicts, and a route to the record).
|
|
58
|
+
Either way one button, never a prompt box.
|
|
46
59
|
2. **Evidence for an in-app decision** (cross-check, audit, tie-out → nothing is written; the
|
|
47
60
|
operator acts on what was found) → **in-app agent** + **`Finding`**. Still bounded and
|
|
48
61
|
prompt-free (an optional instructions brief at most): the brief is fixed, the output is
|
|
@@ -369,12 +382,17 @@ list renders nothing.
|
|
|
369
382
|
|
|
370
383
|
## Confidence — `Confidence`
|
|
371
384
|
|
|
372
|
-
|
|
373
|
-
phrase
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
385
|
+
Confidence IS a callout (`@lotics/ui/confidence`) — the ONLY form it takes: the three-tick
|
|
386
|
+
meter + level phrase head the block, and the REQUIRED `children` carry the basis — what was
|
|
387
|
+
checked and what came of it ("all 7 references pass their checksum" / "2 of 7 fail (X, Y) —
|
|
388
|
+
verify against the source"). The box stays white with a hairline border at EVERY level — the
|
|
389
|
+
meter is the only colored element (the level already lives in the meter and the phrase; a
|
|
390
|
+
tinted fill would say it a third time). Pass `level` or a 0–1 `score` (≥0.8 high ·
|
|
391
|
+
≥0.5 medium · else low; `levelFromScore` exported). There is NO standalone meter: a level
|
|
392
|
+
without its reasons cannot be acted on, so it cannot be rendered — and where a compact meter
|
|
393
|
+
used to decorate a ranked row or a proposal card, remove it (rank, badges, and severity
|
|
394
|
+
already convey standing). Derive levels HONESTLY (deterministic checks: all pass → high, any
|
|
395
|
+
fail → low) — never render a fabricated score.
|
|
378
396
|
|
|
379
397
|
## Findings — evidence, not writes
|
|
380
398
|
|
|
@@ -447,10 +465,9 @@ ranked set, don't cram it into chat prose:
|
|
|
447
465
|
register's New CTA leads with AI: a `FileDropzone` hero → a short ANALYZE run reads the files →
|
|
448
466
|
`ClarifyWizard` asks the ambiguities the analysis surfaced (its Cancel/Back/Next/Submit riding
|
|
449
467
|
the `DialogFooter` via `ClarifyWizardScope` + `ClarifyWizardActions`) → the IMPORT run
|
|
450
|
-
**creates the records and the dialog ends on the
|
|
451
|
-
|
|
452
|
-
law's creation branch
|
|
453
|
-
record's ordinary verbs. The manual VARIANTS sit below the dropzone
|
|
468
|
+
**creates the records and the dialog ends on the RECEIPT** — one record: summary + named
|
|
469
|
+
verdicts + the open-record action; several: an attention-first register pressing through
|
|
470
|
+
to the records (the one law's creation branch). The manual VARIANTS sit below the dropzone
|
|
454
471
|
as `CardSelectItem`s, each opening the classic create-then-refine form. The template mocks the
|
|
455
472
|
clarify step as TWO app-orchestrated phases (analyze-run → wizard → import-run); a real app
|
|
456
473
|
can do it in ONE run — every app agent carries `ask_user_choice`, so the run PARKS on the
|
package/docs/catalog.md
CHANGED
|
@@ -1065,7 +1065,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1065
1065
|
`AgentProgress`'s expanded panel uses it. Inherits the inverted pattern's quirks (scrollbar
|
|
1066
1066
|
thumb runs opposite; keyboard paging inverted) — same as chat. A full-page chat list is
|
|
1067
1067
|
already inverted and doesn't need it.
|
|
1068
|
-
- **`confidence`** — `Confidence` + `ConfidenceLevel` + `levelFromScore`:
|
|
1068
|
+
- **`confidence`** — `Confidence` + `ConfidenceLevel` + `levelFromScore`: confidence IS a
|
|
1069
|
+
callout — the meter + level phrase head the block, the REQUIRED `children` carry the basis
|
|
1070
|
+
(what was checked, what failed); the box stays white with a hairline border at every
|
|
1071
|
+
level — the meter is the only colored element. There is NO standalone meter — a level without its reasons cannot
|
|
1072
|
+
be rendered; where a compact meter used to decorate a ranked row or proposal card, rank /
|
|
1073
|
+
badges / severity carry the standing instead. Also: calibrated
|
|
1069
1074
|
high/med/low; localized via the provider.
|
|
1070
1075
|
- **`change_review`** — the COMPOUND review family — frame: `ChangeReview` provider/stack ·
|
|
1071
1076
|
`ChangeReviewHeader` (auto kept-counter over decidable entries) · `ChangeReviewActions`
|
|
@@ -1101,6 +1106,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1101
1106
|
- **`finding`** — `Finding` + `FindingComparison` + `FindingSeverity`/`FindingLabels`: one
|
|
1102
1107
|
ranked AI-check insight — severity word · title · detail · `Sources` · children slot; the
|
|
1103
1108
|
expected-vs-actual body with the emphasized delta; the `finding` locale slice.
|
|
1109
|
+
- **`result_header`** — `ResultHeader` (+ `ResultTone`): the save-direct RECEIPT's outcome
|
|
1110
|
+
strip, on the page grid — tone mark (`ok`/`attention`/`error`/`skipped`) inline on the
|
|
1111
|
+
title row · outcome-first title · right-slot `action` (the open-record jump / a retry) ·
|
|
1112
|
+
ONE line under (the honest `Confidence`, or the failure reason). The receipt composes:
|
|
1113
|
+
header → the receipt lines (a `DetailTable` of spread `DetailRow`s — label left, value at
|
|
1114
|
+
the right edge) → a `Confidence` callout naming the exact values that failed their
|
|
1115
|
+
deterministic checks (or stating what passed). NOTHING in a
|
|
1116
|
+
receipt edits — the RECORD is the edit surface, one press away. SEVERAL records → an
|
|
1117
|
+
attention-first register of `ListItem` rows (tone mark · title · figures · needs-checking
|
|
1118
|
+
count) pressing straight through to the records. See ai_patterns §the one law.
|
|
1104
1119
|
- **`comments_thread`** — `CommentList` + the
|
|
1105
1120
|
`ThreadComment`/`ThreadMember`/`ThreadFile` types: the record comments thread.
|
|
1106
1121
|
Composing is THE kit `Composer` (never a bespoke comment box): attach via its
|
|
@@ -55,6 +55,8 @@ import { Dialog, DialogFooter, DialogHeader, DialogHeaderTitle, DialogScrollArea
|
|
|
55
55
|
import { AgentRun } from "@lotics/ui/agent_run";
|
|
56
56
|
import { FollowScroll } from "@lotics/ui/follow_scroll";
|
|
57
57
|
import { ClarifyWizard, ClarifyWizardActions, ClarifyWizardScope, type ClarifyWizardAnswer, type ClarifyWizardQuestion } from "@lotics/ui/clarify_wizard";
|
|
58
|
+
import { ResultHeader } from "@lotics/ui/result_header";
|
|
59
|
+
import { Confidence } from "@lotics/ui/confidence";
|
|
58
60
|
import { CardSelectItem } from "@lotics/ui/card_select_item";
|
|
59
61
|
import { FileDropzone } from "@lotics/ui/file_dropzone";
|
|
60
62
|
import { FileDropTarget } from "@lotics/ui/file_drop_target";
|
|
@@ -1141,8 +1143,12 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1141
1143
|
// on the created records (create + link, leave unlinked, or the user's own
|
|
1142
1144
|
// instruction via the custom slot).
|
|
1143
1145
|
const [customerPlan, setCustomerPlan] = useState("New customer created and linked");
|
|
1144
|
-
// the
|
|
1145
|
-
|
|
1146
|
+
// the RECEIPT — what the import CREATED. A real app READS BACK what the
|
|
1147
|
+
// save stored (values + record ids) and runs its DETERMINISTIC checks over
|
|
1148
|
+
// the stored values (a reference checksum, a count reconciliation). ONE
|
|
1149
|
+
// record renders its receipt directly; several render the attention-first
|
|
1150
|
+
// register whose rows press straight through to the records.
|
|
1151
|
+
const [landed, setLanded] = useState<{ id: string; title: string; detail: string; ref: string; refFlagged: boolean; fee: string }[]>([]);
|
|
1146
1152
|
// form state (the manual gate)
|
|
1147
1153
|
const [khach, setKhach] = useState("");
|
|
1148
1154
|
const [dienThoai, setDienThoai] = useState("");
|
|
@@ -1182,13 +1188,20 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1182
1188
|
setTimeout(() => {
|
|
1183
1189
|
const proposals = grouping === "customer" ? PROPOSED_BY_CUSTOMER : PROPOSED_BY_ORDER;
|
|
1184
1190
|
onCreateMany(proposals.map((p) => ({ khach: p.khach, dienThoai: p.dienThoai, phi: p.phi })));
|
|
1185
|
-
|
|
1191
|
+
const rows = proposals.map((p) => ({
|
|
1192
|
+
id: p.id,
|
|
1186
1193
|
title: grouping === "customer" ? p.khach : `${p.orders} — ${p.khach}`,
|
|
1187
1194
|
// The wizard's answers are load-bearing: the unknown customer's row
|
|
1188
1195
|
// states how it landed (created + linked / left unassigned / the
|
|
1189
1196
|
// user's own instruction).
|
|
1190
|
-
detail: [p.
|
|
1191
|
-
|
|
1197
|
+
detail: [p.dienThoai, formatMoney(p.phi), p.khach === "Blue Harbor Foods" ? customerPlan : ""].filter(Boolean).join(" · "),
|
|
1198
|
+
ref: p.orders,
|
|
1199
|
+
// The mock flags one record's reference (a failed checksum); a
|
|
1200
|
+
// real app computes this over the STORED value.
|
|
1201
|
+
refFlagged: p.khach === "Blue Harbor Foods",
|
|
1202
|
+
fee: String(p.phi),
|
|
1203
|
+
}));
|
|
1204
|
+
setLanded(rows);
|
|
1192
1205
|
setPhase("done");
|
|
1193
1206
|
}, 500);
|
|
1194
1207
|
}
|
|
@@ -1338,28 +1351,61 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1338
1351
|
) : null}
|
|
1339
1352
|
|
|
1340
1353
|
{phase === "done" ? (
|
|
1341
|
-
// The
|
|
1342
|
-
//
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1347
|
-
// circle-alert amber-600 · skipped = minus zinc-400. A real app adds
|
|
1348
|
-
// an open-record jump / retry Button on the row's right.
|
|
1354
|
+
// The RECEIPT (ai_patterns §the one law): states the outcome and
|
|
1355
|
+
// routes — NOTHING here edits; the RECORD is the edit surface,
|
|
1356
|
+
// one press away (correction uses the record's ordinary verbs).
|
|
1357
|
+
// ONE record: header + the big-four summary + amber verdicts naming
|
|
1358
|
+
// the exact failing values. SEVERAL: an attention-first register
|
|
1359
|
+
// whose rows press straight through to the record.
|
|
1349
1360
|
<DialogScrollArea>
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1361
|
+
{(() => {
|
|
1362
|
+
// The receipt lines — DetailRow spread rows (label left, value
|
|
1363
|
+
// right), the kit's paper-receipt look; a warning Callout names
|
|
1364
|
+
// the failing values. Read-only: the record behind the dialog
|
|
1365
|
+
// is the edit surface.
|
|
1366
|
+
const receipt = (l: (typeof landed)[number]) => (
|
|
1367
|
+
<View style={{ gap: 16 }}>
|
|
1368
|
+
<ResultHeader
|
|
1369
|
+
tone={l.refFlagged ? "attention" : "ok"}
|
|
1370
|
+
title={`Created ${l.title}`}
|
|
1371
|
+
/>
|
|
1372
|
+
<DetailTable>
|
|
1373
|
+
<DetailRow label="Customer"><Text size="sm" weight="medium">{l.title}</Text></DetailRow>
|
|
1374
|
+
<DetailRow label="Orders"><Text size="sm" weight="medium">{l.ref}</Text></DetailRow>
|
|
1375
|
+
<DetailRow label="Service fee"><Text size="sm" weight="medium" tabular>{formatMoney(Number(l.fee) || 0)}</Text></DetailRow>
|
|
1376
|
+
</DetailTable>
|
|
1377
|
+
{l.refFlagged ? (
|
|
1378
|
+
<Confidence level="low">
|
|
1379
|
+
Order reference PO-7313 fails its checksum — verify against the source file; the flagged value is marked on the record.
|
|
1380
|
+
</Confidence>
|
|
1381
|
+
) : (
|
|
1382
|
+
<Confidence level="high">
|
|
1383
|
+
All automated checks passed: the order references validate and the totals reconcile.
|
|
1384
|
+
</Confidence>
|
|
1385
|
+
)}
|
|
1360
1386
|
</View>
|
|
1361
|
-
)
|
|
1362
|
-
|
|
1387
|
+
);
|
|
1388
|
+
if (landed.length === 1) return receipt(landed[0]);
|
|
1389
|
+
return (
|
|
1390
|
+
<View style={{ gap: 2 }}>
|
|
1391
|
+
{[...landed].sort((a, b) => Number(b.refFlagged) - Number(a.refFlagged)).map((l) => (
|
|
1392
|
+
<ListItem
|
|
1393
|
+
key={l.id}
|
|
1394
|
+
left={<Icon name={l.refFlagged ? "circle-alert" : "circle-check"} size={18} color={l.refFlagged ? colors.amber[600] : colors.green[600]} />}
|
|
1395
|
+
title={`Created ${l.title}`}
|
|
1396
|
+
description={l.detail}
|
|
1397
|
+
right={
|
|
1398
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
1399
|
+
{l.refFlagged ? <Text size="xs" weight="medium" style={{ color: colors.amber[600] }}>1 to check</Text> : null}
|
|
1400
|
+
<Icon name="chevron-right" size={16} color={colors.zinc[400]} />
|
|
1401
|
+
</View>
|
|
1402
|
+
}
|
|
1403
|
+
onPress={() => { close(); }}
|
|
1404
|
+
/>
|
|
1405
|
+
))}
|
|
1406
|
+
</View>
|
|
1407
|
+
);
|
|
1408
|
+
})()}
|
|
1363
1409
|
</DialogScrollArea>
|
|
1364
1410
|
) : null}
|
|
1365
1411
|
|
package/examples/tpl_lookup.tsx
CHANGED
|
@@ -229,8 +229,6 @@ export function TplLookup() {
|
|
|
229
229
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
230
230
|
<Text size="sm" weight="semibold" tabular numberOfLines={1}>{c.code}</Text>
|
|
231
231
|
{c.recommended ? <Badge color="blue" label="Recommended" /> : null}
|
|
232
|
-
<View style={{ flex: 1 }} />
|
|
233
|
-
<Confidence level={c.confidence} />
|
|
234
232
|
</View>
|
|
235
233
|
<Text size="xs" color="muted" numberOfLines={1}>{c.label}</Text>
|
|
236
234
|
</PressableHighlight>
|
|
@@ -256,12 +254,19 @@ export function TplLookup() {
|
|
|
256
254
|
<View style={{ gap: 4 }}>
|
|
257
255
|
{/* verdict header — open, no card box: the panel IS the answer */}
|
|
258
256
|
<View style={{ paddingVertical: 6, gap: 10 }}>
|
|
259
|
-
<
|
|
260
|
-
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Suggested HS code</Text>
|
|
261
|
-
<Confidence level={active.confidence} />
|
|
262
|
-
</View>
|
|
257
|
+
<Text size="xs" color="muted" weight="medium">Suggested HS code</Text>
|
|
263
258
|
<Text size="xxxl" weight="semibold" tabular>{active.code}</Text>
|
|
264
259
|
<Text size="sm" color="muted">{active.heading}</Text>
|
|
260
|
+
{/* Confidence IS a callout — the level with its basis, never a
|
|
261
|
+
floating meter. Ranked rows carry no meter at all: the rank
|
|
262
|
+
and the Recommended badge already convey standing. */}
|
|
263
|
+
<Confidence level={active.confidence}>
|
|
264
|
+
{active.confidence === "high"
|
|
265
|
+
? "The description matches the heading notes and both classification criteria."
|
|
266
|
+
: active.confidence === "medium"
|
|
267
|
+
? "The material matches, but the heading notes leave the use-case ambiguous — check the criteria below."
|
|
268
|
+
: "Only a partial match against the heading notes — treat as a starting point and verify the criteria."}
|
|
269
|
+
</Confidence>
|
|
265
270
|
</View>
|
|
266
271
|
|
|
267
272
|
<Divider />
|
|
@@ -31,7 +31,7 @@ import { AgentRun } from "@lotics/ui/agent_run";
|
|
|
31
31
|
import { Change, ChangeReview, ChangeReviewActions, ChangeReviewHeader, ChangeSummary, type ChangeStatus } from "@lotics/ui/change_review";
|
|
32
32
|
import { Dialog, DialogHeader, DialogHeaderTitle, DialogScrollArea, DialogFooter } from "@lotics/ui/dialog";
|
|
33
33
|
import { CompletionState } from "@lotics/ui/completion_state";
|
|
34
|
-
import {
|
|
34
|
+
import { type ConfidenceLevel } from "@lotics/ui/confidence";
|
|
35
35
|
import { toISODate } from "@lotics/ui/format_date";
|
|
36
36
|
import type { UIMessagePart, UIDataTypes, UITools } from "ai";
|
|
37
37
|
|
|
@@ -442,7 +442,6 @@ function TaskProposal({ proposal, onEdit }: { proposal: Proposal; onEdit: (id: s
|
|
|
442
442
|
<View style={{ flex: 1 }}>
|
|
443
443
|
<InlineTextInput variant="cell" value={proposal.title} onSave={(v) => onEdit(proposal.id, { title: v })} accessibilityLabel="Task title" />
|
|
444
444
|
</View>
|
|
445
|
-
<View style={{ paddingTop: 10 }}><Confidence level={proposal.confidence} /></View>
|
|
446
445
|
</View>
|
|
447
446
|
<View style={{ width: 220 }}>
|
|
448
447
|
<InlineDatePicker variant="cell" value={proposal.due} optionalTime onSave={(v) => onEdit(proposal.id, { due: v })} placeholder="No date" accessibilityLabel="Due date" />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"./finding": "./src/finding.tsx",
|
|
103
103
|
"./clarify": "./src/clarify.tsx",
|
|
104
104
|
"./clarify_wizard": "./src/clarify_wizard.tsx",
|
|
105
|
+
"./result_header": "./src/result_header.tsx",
|
|
105
106
|
"./choice_list": "./src/choice_list.tsx",
|
|
106
107
|
"./sources": "./src/sources.tsx",
|
|
107
108
|
"./icon": "./src/icon.tsx",
|
package/src/confidence.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
1
2
|
import { StyleSheet, View } from "react-native";
|
|
2
3
|
import { colors, solid, type ColorName } from "./colors";
|
|
3
4
|
import { Text } from "./text";
|
|
@@ -6,59 +7,74 @@ import { useLoticsLocale } from "./locale";
|
|
|
6
7
|
export type ConfidenceLevel = "high" | "medium" | "low";
|
|
7
8
|
|
|
8
9
|
export interface ConfidenceLabels {
|
|
9
|
-
/** The full level phrase —
|
|
10
|
-
* so the
|
|
10
|
+
/** The full level phrase — heads the callout and is read out. Default "High confidence" etc.,
|
|
11
|
+
* so the block reads as what it is, not a bare "High". One phrase per level (translates cleanly). */
|
|
11
12
|
high: string;
|
|
12
13
|
medium: string;
|
|
13
14
|
low: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
/** Pass a level directly, or a 0–1 `score` (≥0.8 high · ≥0.5 medium · else low). */
|
|
18
|
-
level?: ConfidenceLevel;
|
|
19
|
-
score?: number;
|
|
20
|
-
/** Translated level phrases. Defaults to English ("High confidence" …). */
|
|
21
|
-
labels?: Partial<ConfidenceLabels>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const FILLED: Record<ConfidenceLevel, number> = { high: 3, medium: 2, low: 1 };
|
|
25
|
-
// high emerald, medium amber, low zinc (unsure — not an error). One family per
|
|
26
|
-
// level; the fill count AND the colour both carry it.
|
|
27
|
-
const COLOR: Record<ConfidenceLevel, ColorName> = { high: "emerald", medium: "amber", low: "zinc" };
|
|
28
|
-
|
|
17
|
+
/** A 0–1 score folded onto the three levels: >=0.8 high · >=0.5 medium · else low. */
|
|
29
18
|
export function levelFromScore(score: number): ConfidenceLevel {
|
|
30
19
|
if (score >= 0.8) return "high";
|
|
31
20
|
if (score >= 0.5) return "medium";
|
|
32
21
|
return "low";
|
|
33
22
|
}
|
|
34
23
|
|
|
24
|
+
const FILLED: Record<ConfidenceLevel, number> = { high: 3, medium: 2, low: 1 };
|
|
25
|
+
const COLOR: Record<ConfidenceLevel, ColorName> = { high: "green", medium: "amber", low: "red" };
|
|
26
|
+
|
|
27
|
+
export interface ConfidenceProps {
|
|
28
|
+
/** Pass a level directly, or a 0–1 `score` (>=0.8 high · >=0.5 medium · else low). */
|
|
29
|
+
level?: ConfidenceLevel;
|
|
30
|
+
score?: number;
|
|
31
|
+
labels?: Partial<ConfidenceLabels>;
|
|
32
|
+
/** The BASIS — what was checked and what came of it ("all 7 references pass
|
|
33
|
+
* their checksum" / "2 of 7 fail (X, Y) — verify against the source").
|
|
34
|
+
* REQUIRED: a confidence with no explanation is decoration. This component
|
|
35
|
+
* IS a callout; there is no standalone meter. */
|
|
36
|
+
children: ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
+
* Confidence WITH its basis, as a callout-shaped block — the ONLY form
|
|
41
|
+
* confidence takes: the meter + level phrase head it, the explanation of what
|
|
42
|
+
* was checked (and what failed) fills it. The box stays WHITE with a hairline
|
|
43
|
+
* border at every level — the meter is the only colored element (the level
|
|
44
|
+
* already lives there and in the phrase; a tinted fill would say it a third
|
|
45
|
+
* time). Never a bare floating meter: a level without its reasons cannot be
|
|
46
|
+
* acted on, so it cannot be rendered.
|
|
40
47
|
*/
|
|
41
48
|
export function Confidence(props: ConfidenceProps) {
|
|
42
|
-
const level = props.level ?? (props.score != null ? levelFromScore(props.score) : "medium");
|
|
49
|
+
const level: ConfidenceLevel = props.level ?? (props.score != null ? levelFromScore(props.score) : "medium");
|
|
43
50
|
const l = { ...useLoticsLocale().confidence, ...props.labels };
|
|
44
51
|
const filled = FILLED[level];
|
|
45
52
|
const fill = solid(COLOR[level]);
|
|
46
53
|
return (
|
|
47
|
-
<View style={styles.
|
|
48
|
-
<View style={styles.
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
<View style={styles.container}>
|
|
55
|
+
<View style={styles.head} accessibilityLabel={l[level]}>
|
|
56
|
+
<View style={styles.meter}>
|
|
57
|
+
{[0, 1, 2].map((i) => (
|
|
58
|
+
<View key={i} style={[styles.tick, { backgroundColor: i < filled ? fill : colors.zinc[200] }]} />
|
|
59
|
+
))}
|
|
60
|
+
</View>
|
|
61
|
+
<Text size="sm" weight="medium">{l[level]}</Text>
|
|
52
62
|
</View>
|
|
53
|
-
<Text size="
|
|
54
|
-
{l[level]}
|
|
55
|
-
</Text>
|
|
63
|
+
<Text size="sm">{props.children}</Text>
|
|
56
64
|
</View>
|
|
57
65
|
);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
const styles = StyleSheet.create({
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
container: {
|
|
70
|
+
padding: 12,
|
|
71
|
+
borderRadius: 10,
|
|
72
|
+
borderWidth: 1,
|
|
73
|
+
borderColor: colors.border,
|
|
74
|
+
backgroundColor: colors.background,
|
|
75
|
+
gap: 4,
|
|
76
|
+
},
|
|
77
|
+
head: { flexDirection: "row", alignItems: "center", gap: 8 },
|
|
78
|
+
meter: { flexDirection: "row", gap: 2, alignItems: "flex-end" },
|
|
63
79
|
tick: { width: 3, height: 10, borderRadius: 1 },
|
|
64
80
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { colors, solid } from "./colors";
|
|
4
|
+
import { Icon, type IconName } from "./icon";
|
|
5
|
+
import { Text, type HeadingLevel } from "./text";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Result header — the strip that opens the POST-SAVE RECEIPT. `ChangeReview` gates a proposal
|
|
9
|
+
* BEFORE it applies; this header opens the receipt for what a save-direct run
|
|
10
|
+
* ALREADY CREATED. The receipt states the outcome and routes — it never edits
|
|
11
|
+
* (correction happens through the record's ordinary verbs, on the record):
|
|
12
|
+
*
|
|
13
|
+
* - ONE record → `ResultHeader` (tone mark inline on the title row · outcome
|
|
14
|
+
* title · failure reason line) + a handful of spread `DetailRow` receipt
|
|
15
|
+
* lines (the identifying figures, not the field wall) + a `Confidence`
|
|
16
|
+
* callout naming exactly which values failed their deterministic checks.
|
|
17
|
+
* - SEVERAL records → an attention-first `ListItem` register whose rows press
|
|
18
|
+
* straight through to the records. Never tabs (they cap out), never stacked
|
|
19
|
+
* collapsed cards, never a field wall duplicating the record page.
|
|
20
|
+
*
|
|
21
|
+
* A flagged value carries a DETERMINISTIC verdict the host computed (a failed
|
|
22
|
+
* checksum, a count that disagrees) — never a fabricated score.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type ResultTone = "ok" | "attention" | "error" | "skipped";
|
|
26
|
+
|
|
27
|
+
const TONE: Record<ResultTone, { icon: IconName; color: string }> = {
|
|
28
|
+
ok: { icon: "circle-check", color: solid("green") },
|
|
29
|
+
attention: { icon: "circle-alert", color: solid("amber") },
|
|
30
|
+
error: { icon: "circle-alert", color: solid("red") },
|
|
31
|
+
skipped: { icon: "minus", color: colors.zinc[400] },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface ResultHeaderProps {
|
|
35
|
+
/** `attention` when any value is flagged (or the item needs the user
|
|
36
|
+
* elsewhere); `error` when the save failed; `skipped` for an item
|
|
37
|
+
* deliberately not processed. The host computes it — it knows its flags. */
|
|
38
|
+
tone: ResultTone;
|
|
39
|
+
/** Outcome-first: "Created <ref>", "Updated <ref>", "Could not save". */
|
|
40
|
+
title: string;
|
|
41
|
+
/** The failure reason (error/skip rows), one line under the title. The
|
|
42
|
+
* record's FIGURES never live here (they belong in the receipt lines), and
|
|
43
|
+
* neither does confidence — that is `Confidence`'s job (a callout, WITH its
|
|
44
|
+
* basis), further down the receipt. */
|
|
45
|
+
detail?: string;
|
|
46
|
+
/** Right-slot action (an open-record jump, a retry) — a `Button`. */
|
|
47
|
+
action?: ReactNode;
|
|
48
|
+
/** Heading rank for the outcome title (screen readers jump by heading).
|
|
49
|
+
* Defaults to 2 — the receipt heads its surface, like `SectionHeadingTitle`. */
|
|
50
|
+
level?: HeadingLevel;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The record's outcome strip — the tone mark rides INLINE on the title row
|
|
54
|
+
* (everything sits on the page grid; no hanging gutter), with ONE meaningful
|
|
55
|
+
* line under it (the honest confidence, or the failure reason) and the action
|
|
56
|
+
* pinned right. Sections of the review (`DetailTable` fields, `Table` rows)
|
|
57
|
+
* follow at the same left edge. */
|
|
58
|
+
export function ResultHeader(props: ResultHeaderProps) {
|
|
59
|
+
const { tone, title, detail, action, level = 2 } = props;
|
|
60
|
+
const t = TONE[tone];
|
|
61
|
+
return (
|
|
62
|
+
<View style={styles.header}>
|
|
63
|
+
<View style={styles.headerTitleRow}>
|
|
64
|
+
<Icon name={t.icon} size={20} color={t.color} />
|
|
65
|
+
<Text level={level} size="md" weight="semibold" style={styles.headerTitle}>{title}</Text>
|
|
66
|
+
{action}
|
|
67
|
+
</View>
|
|
68
|
+
{detail ? (
|
|
69
|
+
<Text size="sm" color={tone === "error" ? "danger" : "muted"}>{detail}</Text>
|
|
70
|
+
) : null}
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const styles = StyleSheet.create({
|
|
76
|
+
header: { gap: 6 },
|
|
77
|
+
headerTitleRow: { flexDirection: "row", alignItems: "center", gap: 10 },
|
|
78
|
+
headerTitle: { flex: 1, minWidth: 0 },
|
|
79
|
+
});
|