@lotics/ui 4.9.0 → 5.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/AGENTS.md +75 -34
- package/examples/tpl_assistant.tsx +25 -4
- package/examples/tpl_dieline.tsx +2 -2
- package/examples/tpl_extract.tsx +18 -10
- package/examples/tpl_lookup.tsx +80 -67
- package/examples/tpl_match.tsx +25 -14
- package/examples/tpl_ratedesk.tsx +375 -0
- package/package.json +4 -4
- package/src/agent_run.tsx +1 -1
- package/src/change_review.tsx +103 -96
- package/src/combobox.tsx +8 -1
- package/src/confidence.tsx +1 -1
- package/src/control_surface.ts +27 -1
- package/src/file_thumbnail.tsx +32 -1
- package/src/icon.tsx +2 -0
- package/src/match_sides.tsx +79 -0
- package/src/record_fields.tsx +68 -0
- package/src/review_card.tsx +172 -0
- package/src/match_row.tsx +0 -133
- package/src/record_review.tsx +0 -149
- package/src/suggestion.tsx +0 -92
package/AGENTS.md
CHANGED
|
@@ -80,13 +80,14 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
80
80
|
- **AI surfaces** — `Composer` (the adaptive command/chat composer — a compact pill when empty that
|
|
81
81
|
expands for long text + attachments; the surface that triggers agent work),
|
|
82
82
|
`AgentRun` (the live streaming work feed) + `AgentProgress` (its compact, floating, expandable
|
|
83
|
-
form — a composer's "working" state), `
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`
|
|
83
|
+
form — a composer's "working" state), `ReviewCard` (THE single-proposal review card — compositional
|
|
84
|
+
body via `children`: a `RecordFields` record (header-first) or a `MatchSides` pairing (body-first))
|
|
85
|
+
+ `Confidence`; `ChangeReview` (the BATCH review engine — whole-set OR 1-by-1; compositional via
|
|
86
|
+
`renderItem`, with `ChangeDiff` for the before→after body),
|
|
87
|
+
`Clarify` (the agent asks back — selectable `ChoiceList` options),
|
|
88
|
+
`Sources` (provenance chips for AI output). For STRUCTURED AI output the review
|
|
88
89
|
surfaces above don't cover: `SpecList` (the "exact breakdown" — a labeled-value spec sheet, the
|
|
89
|
-
panel of an answer),
|
|
90
|
+
panel of an answer),
|
|
90
91
|
`Finding` (one ranked briefing/audit insight — severity + metric + sources + action), `Discrepancy`
|
|
91
92
|
(a field whose value disagrees across sources — pick the truth), `TriageRow` (an incoming item the
|
|
92
93
|
agent classified + routed), `ScoredOption` (an AI-ranked candidate — score + rationale + specs).
|
|
@@ -200,7 +201,11 @@ callbacks: `onFilePress` → set a `number|null` index that drives `<FileGallery
|
|
|
200
201
|
activeIndex onIndexChange>` (built-in prev/next/ESC/rotate); `onRemove` → drop the file from state
|
|
201
202
|
(the grid renders a ✕ on each thumbnail automatically). Keep label+grid+dropzone on a `gap` (a bare
|
|
202
203
|
`CardBody` has none). `FileThumbnail` shows the right surface per MIME (image thumbnail · doc card ·
|
|
203
|
-
media card)
|
|
204
|
+
media card), and takes `uploading` to dim it with a spinner while the bytes are still going up.
|
|
205
|
+
Never hand-build a drop well, a file row grid, or a preview modal. In a custom-code APP, don't
|
|
206
|
+
hand-roll the upload+preview state either — `@lotics/app-sdk` `useAttachments()` gives the chat's
|
|
207
|
+
instant-preview lifecycle (local object-URL now, `file_id` on complete); map its `files` to
|
|
208
|
+
`<FileThumbnail uploading={f.status === "uploading"} />`.
|
|
204
209
|
|
|
205
210
|
### Quick capture — the SPEED surface
|
|
206
211
|
For fast repeat entry (logging activity, expenses), one compact capture row (a `SegmentedControl`
|
|
@@ -243,15 +248,23 @@ Compose the surfaces as a loop, and reach for the right one by job:
|
|
|
243
248
|
intelligence is legible. On a canvas/composer app reach for `AgentProgress` — `AgentRun`
|
|
244
249
|
collapsed into a floating pill (avatar + current step) that EXPANDS on press; the composer
|
|
245
250
|
morphs into it while running, and reveals again when done.
|
|
246
|
-
- **Review before apply
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
251
|
+
- **Review before apply — ONE card + the batch engine, the BODY is compositional.** Every "the agent
|
|
252
|
+
proposes → the human accepts / edits / dismisses → nothing auto-applies" surface wears the same
|
|
253
|
+
`reviewCardStyle` + an optional `Confidence`. There are two primitives:
|
|
254
|
+
- **`ReviewCard`** — THE single-proposal review card. It owns the chrome (eyebrow + `Confidence`,
|
|
255
|
+
rationale, a generic `actions` footer — Accept/Edit/Dismiss, Confirm/Remove, Accept/Reassign,
|
|
256
|
+
whatever the verbs are — and a collapse-to-a-row resolved state); the BODY is `children`. **Layout
|
|
257
|
+
follows the props**: give it an `eyebrow`/`title` and the chrome LEADS (header-first — a value or
|
|
258
|
+
record proposal, e.g. a `RecordFields` → `tpl_extract`); give it only `children` and the BODY leads,
|
|
259
|
+
the confidence + reason beneath it (body-first — a `MatchSides` pairing where the link is the hero
|
|
260
|
+
→ `tpl_match`). Stack several (rank by confidence) for a shortlist or a document's records.
|
|
261
|
+
- **`ChangeReview`** — THE batch engine for a SET you decide each then commit together. `renderItem`
|
|
262
|
+
draws each proposal (a `ChangeDiff` before→after body, a record, an editable form), 1-by-1
|
|
263
|
+
Keep/Drop collapses each, "Accept all" + Apply commits only the kept (e.g. ingested rate lines —
|
|
264
|
+
`tpl_ratedesk`); whole-set mode applies/discards the lot.
|
|
265
|
+
The body atoms (`RecordFields`, `MatchSides`, `ChangeDiff`) are pure presentation — usable inside
|
|
266
|
+
either engine OR standalone (a live-edit field card). The decide / accept-all / apply mechanics are
|
|
267
|
+
never re-implemented per surface.
|
|
255
268
|
- **Ask back** — `Clarify`: when the agent is unsure, it asks a question with quick-reply options
|
|
256
269
|
and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run.
|
|
257
270
|
- **Provenance** — `Sources`: openable chips saying where the output came FROM (records, a
|
|
@@ -267,7 +280,7 @@ a conversation, a single pass, or generated prose:
|
|
|
267
280
|
- **Canvas** (`tpl_dieline`) — the page IS the design on a pannable/zoomable surface: it stays
|
|
268
281
|
CENTRED at every zoom (a floating zoom pill bottom-left), the FLOATING composer at the bottom (a
|
|
269
282
|
`Composer`: attach a photo → review/remove it → add a note → send, with `sendDisabled` allowing a
|
|
270
|
-
file-and/or-text submit — NOT auto-run on attach) morphs into `AgentProgress` while running, and a pinned PARAMS PANEL (
|
|
283
|
+
file-and/or-text submit — NOT auto-run on attach) morphs into `AgentProgress` while running, and a pinned PARAMS PANEL (a `RecordFields` field-card
|
|
271
284
|
in live-edit mode, carrying the single Download; minimizes to a pill) floats centre-right. Panel, zoom pill and composer
|
|
272
285
|
float ON TOP — they never shift the design. (Floating layers use a `pointerEvents:"none"` wrapper
|
|
273
286
|
with the interactive child set `"auto"`; RN-Web ignores `"box-none"` in style, so a full-width
|
|
@@ -277,22 +290,33 @@ a conversation, a single pass, or generated prose:
|
|
|
277
290
|
- **Chat** (`tpl_assistant`) — a conversation where the agent's answer carries suggested edits the
|
|
278
291
|
user approves/rejects **1-by-1** (`ChangeReview` per-item), applying only the accepted set. An AI
|
|
279
292
|
copilot that proposes changes to a record and never commits on its own.
|
|
280
|
-
- **One-shot** (`tpl_extract
|
|
281
|
-
|
|
282
|
-
Save all to the table. A data-capture pass; nothing stored until confirmed.
|
|
293
|
+
- **One-shot** (`tpl_extract`) — read an image → the agent extracts the records → review each as a
|
|
294
|
+
`ReviewCard` wrapping a `RecordFields` body (fields click-to-edit, Confirm collapses it to a
|
|
295
|
+
checklist row) → Save all to the table. A data-capture pass; nothing stored until confirmed.
|
|
296
|
+
- **Bulk ingest desk** (`tpl_ratedesk`) — the page IS a filterable register (a From → To route picker
|
|
297
|
+
+ `ColumnFilter` pills → `Card padding:0` + full-bleed `Table`), the register the hero. Updating
|
|
298
|
+
prices is ONE header CTA that opens a Dialog wizard: drop SEVERAL source docs at once → the agent
|
|
299
|
+
reads them all (`AgentRun`) → a `ChangeReview` 1-by-1 (each proposal a Keep/Drop card, New / Updated
|
|
300
|
+
with old→new delta, "Accept all" + Apply) → the kept rows land in the register. Pairs `tpl_extract`'s
|
|
301
|
+
ingest with `tpl_directory`'s register. For a maintained reference table — rate card, price list,
|
|
302
|
+
tariff, catalogue: re-ingest the source sheets beats hand-typing.
|
|
283
303
|
- **Draft** (`tpl_draft`) — the agent writes a first draft from context; the OUTPUT itself streams
|
|
284
304
|
(the text fills in live), then becomes an editable surface the human refines before sending.
|
|
285
305
|
Tone/length steer it. For generated prose — a reply, a quote, a listing.
|
|
286
306
|
|
|
287
307
|
The next six output STRUCTURE the first four can't — when the answer is a panel of facts, a queue of
|
|
288
308
|
decisions, or a ranked set, don't cram it into chat prose. Each pairs a template with a primitive:
|
|
289
|
-
- **Answer desk** (`tpl_lookup`, `SpecList`) —
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
309
|
+
- **Answer desk** (`tpl_lookup`, `SpecList`) — describe the goods on the LEFT → the agent RANKS the
|
|
310
|
+
matching codes (NEAREST MATCHES, the top one Recommended) → pick one and its STRUCTURED answer pins on
|
|
311
|
+
the RIGHT (a verdict header + an exact breakdown + the policies + sources). Input → matches → pick,
|
|
312
|
+
NOT a single confident verdict: classification is ambiguous, so the alternatives are first-class and
|
|
313
|
+
picking a different one changes the duty; refining the description re-ranks. For look-up-and-explain:
|
|
314
|
+
tariff/HS, fee lookup, policy Q&A, a spec/compliance desk. (NOT a chat with the answer in a bubble.)
|
|
315
|
+
- **Match** (`tpl_match`, body-first `ReviewCard` + `MatchSides`) — the agent proposes PAIRINGS with
|
|
316
|
+
its reasoning; each is a `ReviewCard` (no title, so body-first) whose body is a two-sided `MatchSides`
|
|
317
|
+
pairing (source → proposed counterpart), with the confidence + reason beneath, accept / reassign /
|
|
318
|
+
reject, high-confidence batch-accept. The AI reconciliation / dedup / correlation queue. (The
|
|
319
|
+
deterministic cousin
|
|
296
320
|
`tpl_reconcile` has no agent reasoning — that's the tell for which to reach for.)
|
|
297
321
|
- **Briefing** (`tpl_briefing`, `Finding`) — a generated narrative over the data + a RANKED list of
|
|
298
322
|
what needs attention, each finding carrying a severity, a metric, its sources, and one action. The
|
|
@@ -425,6 +449,19 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
425
449
|
· `FilterPill` is THE secondary-dimension filter — a compact pill that opens a composed popover
|
|
426
450
|
editor (`PickerMenu multi` / `RangeSlider` / `Counter` / date range). Single-select closes on
|
|
427
451
|
pick via the `{({close}) => …}` render prop.
|
|
452
|
+
· `ColumnFilter` is the TYPED column filter — give it a `FilterableColumn` (`text` / `number` /
|
|
453
|
+
`select`) + a controlled `ColumnFilterValue` and it renders the right editor (contains / range /
|
|
454
|
+
multi-select) inside a `FilterPill`, with `columnFilterToConditions` mapping the value to query
|
|
455
|
+
predicates. Reach for it — ONE compact pill per dimension — when a register filters on SEVERAL
|
|
456
|
+
columns (origin, destination, carrier, type…). Never hand-roll a row of bespoke chips/filters,
|
|
457
|
+
and never use a permanently-expanded `ChipGroup` for a secondary dimension.
|
|
458
|
+
· `Combobox pill` is the SEARCHABLE filter in the band — `<Combobox pill>` wears the same white
|
|
459
|
+
toolbar-pill surface as `SearchInput`/`FilterPill` (don't put a bare form-look Combobox in a
|
|
460
|
+
toolbar — its transparent input reads grey on the zinc-50 canvas beside the white pills). Reach
|
|
461
|
+
for it over `ColumnFilter` when the value is ONE of a large/growing set you type to narrow (a
|
|
462
|
+
port, a customer) rather than a short fixed list. Pass `recentOptions` (the same option list) so
|
|
463
|
+
a click opens the full list, then typing filters. The **From → To route picker** is two of them
|
|
464
|
+
(`icon="map-pin"`, `clearable`) with an `arrow-right` between — flight-search style.
|
|
428
465
|
· `Tabs` switch between VIEWS/sections (different content/layout) — never a subset of one list.
|
|
429
466
|
`SegmentedControl` chooses a MODE/PARAMETER of the SAME view (2–4 peers, no panel swap).
|
|
430
467
|
- **Footer actions align RIGHT.** `PopoverFooter`/`DialogFooter`/`DrawerFooter` default
|
|
@@ -490,11 +527,13 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
|
|
|
490
527
|
reveals CENTRED on a pannable/zoomable surface, the floating composer morphing into `AgentProgress`,
|
|
491
528
|
a pinned live-edit params panel centre-right; prompt OR edit a param to iterate) ·
|
|
492
529
|
`tpl_assistant` (a chat copilot: the agent's answer carries suggested edits approved/rejected
|
|
493
|
-
1-by-1) · `tpl_extract` (read an image → extract records → review/edit each (`
|
|
494
|
-
Save all) · `
|
|
530
|
+
1-by-1) · `tpl_extract` (read an image → extract records → review/edit each (`ReviewCard` +
|
|
531
|
+
`RecordFields`) → Save all) · `tpl_ratedesk` (BULK INGEST desk: a register-as-hero with an "Import
|
|
532
|
+
prices" CTA → a Dialog wizard: drop several sheets → `ChangeReview` 1-by-1 → kept rows land in the
|
|
533
|
+
register; for a maintained rate card / price list / tariff) · `tpl_draft` (draft generation: context → the reply text streams into an editable surface
|
|
495
534
|
→ refine → send; tone/length steer it). *Structure*: `tpl_lookup` (the ANSWER desk: converse left,
|
|
496
535
|
a pinned `SpecList` verdict panel right; a follow-up refines it) · `tpl_match` (AI reconciliation:
|
|
497
|
-
`
|
|
536
|
+
body-first `ReviewCard` + `MatchSides` pairings with reasoning, accept/reassign/reject) · `tpl_briefing` (a generated narrative
|
|
498
537
|
+ a ranked `Finding` attention list) · `tpl_crosscheck` (the audit desk: `Discrepancy` cards where
|
|
499
538
|
the documents disagree) · `tpl_triage` (an inbox of `TriageRow`s the agent classified + routed) ·
|
|
500
539
|
`tpl_compare` (ranked `ScoredOption` candidates the agent scored).
|
|
@@ -516,7 +555,9 @@ inline_text_input · inline_number_input · inline_select · inline_member_selec
|
|
|
516
555
|
inline_time_picker (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` +
|
|
517
556
|
`InlineEditView`; `InlineSelect`/`InlineMemberSelect` take `renderValue` for a chip/badge at rest) ·
|
|
518
557
|
list · list_item · menu_button · menu_list_item · detail_row · pressable_row · action_menu ·
|
|
519
|
-
floating_action_bar · filter_pill ·
|
|
558
|
+
floating_action_bar · filter_pill · column_filter (ColumnFilter — the typed per-column filter pill +
|
|
559
|
+
columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
|
|
560
|
+
sort_header · table · pagination · accordion · stepper ·
|
|
520
561
|
step_progress · step_list · timeline · drawer (+ DrawerFooter) · dialog · popover · tooltip ·
|
|
521
562
|
command_menu · alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
|
|
522
563
|
CalloutText · CalloutActions) · kpi_card · kpi_strip · metric · trend_chip · sparkline ·
|
|
@@ -527,11 +568,11 @@ file_gallery_modal · image_gallery · avatar · skeleton · activity_indicator
|
|
|
527
568
|
spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
|
|
528
569
|
comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
|
|
529
570
|
expandable form — a composer's working state) · composer (Composer — the adaptive command/chat
|
|
530
|
-
composer: a compact pill that expands for long text + attachments) ·
|
|
531
|
-
|
|
571
|
+
composer: a compact pill that expands for long text + attachments) · review_card (ReviewCard — THE single-proposal review card; compositional body via children + a generic actions footer; header-first with a title/eyebrow, body-first without) · confidence (calibrated high/med/low) ·
|
|
572
|
+
record_fields (RecordFields — the editable extracted-record BODY for a ReviewCard, or a standalone live-edit field card) · change_review (ChangeReview — the BATCH review engine, whole-set OR 1-by-1; compositional `renderItem` body + the `ChangeDiff` before→after helper) ·
|
|
532
573
|
clarify (the agent asks back, via ChoiceList) · choice_list (ChoiceList — selectable answer options,
|
|
533
574
|
the agent's quick-reply surface) · sources (provenance chips, per-kind glyphs) · spec_list (SpecList — the exact
|
|
534
|
-
labeled-value breakdown, the panel of an answer) ·
|
|
575
|
+
labeled-value breakdown, the panel of an answer) · match_sides (MatchSides — the two-sided pairing BODY for a body-first ReviewCard reconciliation; composable side slots + the MatchSideText default) ·
|
|
535
576
|
finding (Finding — one ranked briefing/audit insight) · discrepancy (Discrepancy — a field that
|
|
536
577
|
disagrees across sources) · triage_row (TriageRow — an item the agent classified + routed) ·
|
|
537
578
|
scored_option (ScoredOption — an AI-ranked candidate) ·
|
|
@@ -4,7 +4,7 @@ import { colors } from "@lotics/ui/colors";
|
|
|
4
4
|
import { Text } from "@lotics/ui/text";
|
|
5
5
|
import { DotsIndicator } from "@lotics/ui/dots_indicator";
|
|
6
6
|
import { Composer } from "@lotics/ui/composer";
|
|
7
|
-
import { ChangeReview,
|
|
7
|
+
import { ChangeReview, ChangeDiff, type ChangeReviewItemStatus } from "@lotics/ui/change_review";
|
|
8
8
|
|
|
9
9
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
10
|
// Template · AI assistant — a CHAT workflow, a different shape from the design
|
|
@@ -14,17 +14,24 @@ import { ChangeReview, type ChangeReviewItem, type ChangeReviewItemStatus } from
|
|
|
14
14
|
// copilot proposing changes to a record, never committing on its own. All mock.
|
|
15
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
|
+
/** One field's before→after edit — the assistant's proposals, reviewed 1-by-1. */
|
|
18
|
+
interface EditChange {
|
|
19
|
+
label: string;
|
|
20
|
+
before?: string;
|
|
21
|
+
after: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
interface ChatMsg {
|
|
18
25
|
id: string;
|
|
19
26
|
role: "user" | "agent";
|
|
20
27
|
text?: string;
|
|
21
28
|
/** An agent turn may carry suggested edits to review 1-by-1. */
|
|
22
|
-
changes?:
|
|
29
|
+
changes?: EditChange[];
|
|
23
30
|
applied?: boolean;
|
|
24
31
|
typing?: boolean;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
const SUGGESTED:
|
|
34
|
+
const SUGGESTED: EditChange[] = [
|
|
28
35
|
{ label: "Company", before: "acme packaging co", after: "Acme Packaging Co." },
|
|
29
36
|
{ label: "Phone", before: "0901234567", after: "+84 90 123 4567" },
|
|
30
37
|
{ label: "Email", before: "SALES@ACME.VN", after: "sales@acme.vn" },
|
|
@@ -109,7 +116,21 @@ export function TplAssistant() {
|
|
|
109
116
|
{m.text ? <Text size="sm">{m.text}</Text> : null}
|
|
110
117
|
{m.changes ? (
|
|
111
118
|
<ChangeReview
|
|
112
|
-
|
|
119
|
+
items={m.changes}
|
|
120
|
+
getKey={(c, i) => `${m.id}:${i}`}
|
|
121
|
+
statusOf={(_, i) => decisions[`${m.id}:${i}`] ?? "pending"}
|
|
122
|
+
renderItem={(c) => (
|
|
123
|
+
<>
|
|
124
|
+
<Text size="xs" color="muted" weight="medium">{c.label}</Text>
|
|
125
|
+
<ChangeDiff before={c.before} after={c.after} />
|
|
126
|
+
</>
|
|
127
|
+
)}
|
|
128
|
+
renderSummary={(c, kept) => (
|
|
129
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
130
|
+
<Text size="sm" weight="medium" numberOfLines={1}>{c.label}</Text>
|
|
131
|
+
<Text size="sm" numberOfLines={1} color={kept ? "default" : "muted"} style={[{ flex: 1 }, kept ? undefined : { textDecorationLine: "line-through" }]}>{kept ? c.after : (c.before ?? c.after)}</Text>
|
|
132
|
+
</View>
|
|
133
|
+
)}
|
|
113
134
|
status={m.applied ? "applied" : "open"}
|
|
114
135
|
applyLabel="Apply accepted"
|
|
115
136
|
onAcceptItem={active ? (i) => decide(m.id, i, "accepted") : undefined}
|
package/examples/tpl_dieline.tsx
CHANGED
|
@@ -24,7 +24,7 @@ const MOCK_PHOTO: DisplayFile = {
|
|
|
24
24
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
25
|
// Template · AI design canvas (the carton "dieline" app) — the FLAGSHIP shape:
|
|
26
26
|
// the page IS the design. The dieline fills the centre, a FLOATING composer sits
|
|
27
|
-
// at the bottom, and a pinned PARAMS PANEL (
|
|
27
|
+
// at the bottom, and a pinned PARAMS PANEL (a RecordFields field-card, here in
|
|
28
28
|
// live-edit mode) floats at the centre-right. Empty → drop a photo in the
|
|
29
29
|
// composer → the centre processes while the composer MORPHS into a progress pill
|
|
30
30
|
// (AgentProgress) → the dieline reveals, the params panel appears, the composer
|
|
@@ -228,7 +228,7 @@ export function TplDieline() {
|
|
|
228
228
|
);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
// The pinned right-rail —
|
|
231
|
+
// The pinned right-rail — a RecordFields field-card in live-edit mode: each
|
|
232
232
|
// parameter is click-to-edit and re-flows the design on save; one primary
|
|
233
233
|
// Download. Floats over the canvas, so it carries the composer's lift.
|
|
234
234
|
const PARAMS: { key: keyof Dims; label: string }[] = [
|
package/examples/tpl_extract.tsx
CHANGED
|
@@ -6,7 +6,8 @@ import { Button } from "@lotics/ui/button";
|
|
|
6
6
|
import { Card, CardBody, CardHeader, CardHeaderTitle } from "@lotics/ui/card";
|
|
7
7
|
import { FileDropzone } from "@lotics/ui/file_dropzone";
|
|
8
8
|
import { AgentRun, type AgentRunStep } from "@lotics/ui/agent_run";
|
|
9
|
-
import {
|
|
9
|
+
import { ReviewCard } from "@lotics/ui/review_card";
|
|
10
|
+
import { RecordFields, type RecordField } from "@lotics/ui/record_fields";
|
|
10
11
|
import { Sources } from "@lotics/ui/sources";
|
|
11
12
|
import { CompletionState } from "@lotics/ui/completion_state";
|
|
12
13
|
import { type ConfidenceLevel } from "@lotics/ui/confidence";
|
|
@@ -14,8 +15,8 @@ import { type ConfidenceLevel } from "@lotics/ui/confidence";
|
|
|
14
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
16
|
// Template · AI extract & confirm — read an image, then review each extracted
|
|
16
17
|
// RECORD before it's added to a table. Drop a photo → the agent extracts the
|
|
17
|
-
// lines (AgentRun) → each lands as a `
|
|
18
|
-
// click-to-edit; Confirm collapses it to a tidy checklist row (Undo to re-open),
|
|
18
|
+
// lines (AgentRun) → each lands as a `ReviewCard` wrapping a `RecordFields` body
|
|
19
|
+
// whose fields are click-to-edit; Confirm collapses it to a tidy checklist row (Undo to re-open),
|
|
19
20
|
// Remove skips it → Save all writes the confirmed lines at once. A one-shot
|
|
20
21
|
// capture pass: nothing is stored until the human has reviewed it. All mock.
|
|
21
22
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -145,17 +146,24 @@ export function TplExtract() {
|
|
|
145
146
|
<Sources label="Extracted from" onOpen={() => {}} sources={[{ id: "doc", label: "packing-list.jpg", kind: "document", detail: "1 photo" }]} />
|
|
146
147
|
<View style={{ gap: 10 }}>
|
|
147
148
|
{RECORDS.map((r) => (
|
|
148
|
-
<
|
|
149
|
+
<ReviewCard
|
|
149
150
|
key={r.id}
|
|
150
151
|
title={r.title}
|
|
151
152
|
confidence={r.confidence}
|
|
152
|
-
status={statuses[r.id]
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
status={statuses[r.id] === "confirmed" ? "accepted" : statuses[r.id] === "removed" ? "dismissed" : "open"}
|
|
154
|
+
acceptedLabel="Confirmed"
|
|
155
|
+
dismissedLabel="Removed"
|
|
156
|
+
actions={[
|
|
157
|
+
{ label: "Remove", onPress: () => setStatus(r.id, "removed"), kind: "muted" },
|
|
158
|
+
{ label: "Confirm", onPress: () => setStatus(r.id, "confirmed"), kind: "secondary" },
|
|
159
|
+
]}
|
|
157
160
|
onUndo={() => setStatus(r.id, undefined)}
|
|
158
|
-
|
|
161
|
+
>
|
|
162
|
+
<RecordFields
|
|
163
|
+
fields={r.fields.map((f, i) => ({ ...f, value: values[r.id][i] }))}
|
|
164
|
+
onEditField={(i, value) => editField(r.id, i, value)}
|
|
165
|
+
/>
|
|
166
|
+
</ReviewCard>
|
|
159
167
|
))}
|
|
160
168
|
</View>
|
|
161
169
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
package/examples/tpl_lookup.tsx
CHANGED
|
@@ -9,23 +9,29 @@ import { Composer } from "@lotics/ui/composer";
|
|
|
9
9
|
import { AgentRun, type AgentRunStep } from "@lotics/ui/agent_run";
|
|
10
10
|
import { SpecList, type SpecRow } from "@lotics/ui/spec_list";
|
|
11
11
|
import { Confidence, type ConfidenceLevel } from "@lotics/ui/confidence";
|
|
12
|
+
import { Badge } from "@lotics/ui/badge";
|
|
12
13
|
import { Sources, type SourceRef } from "@lotics/ui/sources";
|
|
13
14
|
|
|
14
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
-
// Template · Answer desk —
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
16
|
+
// Template · Answer desk — describe the goods on the LEFT, the agent RANKS the
|
|
17
|
+
// matching codes (nearest matches, the top one Recommended), you pick one and its
|
|
18
|
+
// structured ANSWER pins on the RIGHT. The flow is input → matches → pick, not a
|
|
19
|
+
// single confident verdict: classification is ambiguous, so the alternatives are
|
|
20
|
+
// first-class (the prep/pack push the goods to a different code with a different
|
|
21
|
+
// duty). The answer is a panel of scannable components — the code, the exact duty
|
|
22
|
+
// breakdown (SpecList), the policies, the sources. Refine the description ("what
|
|
23
|
+
// if it's pre-cooked?") and the ranking updates. Same shape for any
|
|
24
|
+
// look-up-and-explain: tariff/HS, fee lookup, policy Q&A, a spec/compliance desk.
|
|
22
25
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
26
|
|
|
24
27
|
type Tone = "warning" | "info" | "success";
|
|
25
28
|
interface Policy { tone: Tone; title: string; text: string }
|
|
26
|
-
interface
|
|
29
|
+
interface Candidate {
|
|
27
30
|
id: string;
|
|
28
|
-
|
|
31
|
+
/** Short heading for the ranked list on the left. */
|
|
32
|
+
label: string;
|
|
33
|
+
/** The agent's top pick — wears a "Recommended" badge, selected first. */
|
|
34
|
+
recommended?: boolean;
|
|
29
35
|
code: string;
|
|
30
36
|
heading: string;
|
|
31
37
|
confidence: ConfidenceLevel;
|
|
@@ -37,10 +43,16 @@ interface Verdict {
|
|
|
37
43
|
sources: SourceRef[];
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
// One query, several plausible HS codes — the agent RANKS them; the declarant
|
|
47
|
+
// picks. The prep and the pack push the goods to a different code with a
|
|
48
|
+
// different duty, which is exactly why a single confident verdict is wrong.
|
|
49
|
+
const QUERY = "Black tiger shrimp, headless shell-on, IQF frozen, 1 kg retail packs";
|
|
50
|
+
|
|
51
|
+
const CANDIDATES: Candidate[] = [
|
|
41
52
|
{
|
|
42
|
-
id: "
|
|
43
|
-
|
|
53
|
+
id: "c1",
|
|
54
|
+
label: "Frozen shrimp · retail packs ≤ 1 kg",
|
|
55
|
+
recommended: true,
|
|
44
56
|
code: "0306.17.10",
|
|
45
57
|
heading: "Frozen shrimp & prawns of the family Penaeidae — not in airtight containers",
|
|
46
58
|
confidence: "high",
|
|
@@ -69,54 +81,57 @@ const VERDICTS: Verdict[] = [
|
|
|
69
81
|
],
|
|
70
82
|
},
|
|
71
83
|
{
|
|
72
|
-
id: "
|
|
73
|
-
|
|
74
|
-
code: "
|
|
75
|
-
heading: "
|
|
76
|
-
confidence: "
|
|
77
|
-
basis:
|
|
84
|
+
id: "c2",
|
|
85
|
+
label: "Frozen shrimp · other (bulk / catering)",
|
|
86
|
+
code: "0306.17.90",
|
|
87
|
+
heading: "Frozen shrimp & prawns of the family Penaeidae — other",
|
|
88
|
+
confidence: "medium",
|
|
89
|
+
basis:
|
|
90
|
+
"Same heading 0306.17, but the residual national line .90 covers packs ABOVE 1 kg / bulk catering. Right only if the ≤ 1 kg retail-pack evidence is set aside — the duty is identical, the line is not.",
|
|
78
91
|
duty: [
|
|
79
|
-
{ label: "Import duty (MFN)", value: "
|
|
80
|
-
{ label: "ATIGA (Form D)", value: "0%" },
|
|
92
|
+
{ label: "Import duty (MFN)", value: "0%" },
|
|
81
93
|
{ label: "VAT", value: "8%" },
|
|
94
|
+
{ label: "Anti-dumping", value: "0%" },
|
|
82
95
|
],
|
|
83
|
-
dutyTotal: { label: "Effective tax on CIF", value: "8
|
|
96
|
+
dutyTotal: { label: "Effective tax on CIF", value: "8%" },
|
|
84
97
|
specs: [
|
|
85
|
-
{ label: "Chapter", value: "
|
|
86
|
-
{ label: "
|
|
87
|
-
{ label: "
|
|
98
|
+
{ label: "Chapter", value: "03 — Fish & crustaceans" },
|
|
99
|
+
{ label: "Form", value: "Frozen (IQF)" },
|
|
100
|
+
{ label: "Net pack", value: "> 1 kg / bulk" },
|
|
88
101
|
],
|
|
89
102
|
policies: [
|
|
90
|
-
{ tone: "
|
|
103
|
+
{ tone: "warning", title: "Health certificate required", text: "Aquatic-product imports need a health certificate from the competent authority of the exporting country before clearance." },
|
|
91
104
|
],
|
|
92
105
|
sources: [
|
|
93
|
-
{ id: "s1", label: "Tariff schedule 2026", kind: "knowledge", detail: "Ch.
|
|
94
|
-
{ id: "s2", label: "
|
|
106
|
+
{ id: "s1", label: "Tariff schedule 2026", kind: "knowledge", detail: "Ch. 03" },
|
|
107
|
+
{ id: "s2", label: "Heading 0306.17", kind: "knowledge" },
|
|
95
108
|
],
|
|
96
109
|
},
|
|
97
110
|
{
|
|
98
|
-
id: "
|
|
99
|
-
|
|
100
|
-
code: "
|
|
101
|
-
heading: "
|
|
102
|
-
confidence: "
|
|
103
|
-
basis:
|
|
111
|
+
id: "c3",
|
|
112
|
+
label: "Shrimp, prepared or preserved",
|
|
113
|
+
code: "1605.21.00",
|
|
114
|
+
heading: "Shrimp & prawns, prepared or preserved — not in airtight containers",
|
|
115
|
+
confidence: "low",
|
|
116
|
+
basis:
|
|
117
|
+
"Only if the product is COOKED / breaded / marinated → chapter 16 (prepared), heading 1605.21. Raw IQF shrimp is NOT prepared — the wrong line unless processing is confirmed, and it carries a much higher MFN rate.",
|
|
104
118
|
duty: [
|
|
105
|
-
{ label: "Import duty (MFN)", value: "
|
|
106
|
-
{ label: "
|
|
119
|
+
{ label: "Import duty (MFN)", value: "20%" },
|
|
120
|
+
{ label: "ATIGA (Form D)", value: "0%" },
|
|
121
|
+
{ label: "VAT", value: "8%" },
|
|
107
122
|
],
|
|
108
|
-
dutyTotal: { label: "Effective tax on CIF", value: "
|
|
123
|
+
dutyTotal: { label: "Effective tax on CIF", value: "8–28%" },
|
|
109
124
|
specs: [
|
|
110
|
-
{ label: "Chapter", value: "
|
|
111
|
-
{ label: "
|
|
112
|
-
{ label: "
|
|
125
|
+
{ label: "Chapter", value: "16 — Prepared fish / crustaceans" },
|
|
126
|
+
{ label: "Form", value: "Cooked / prepared" },
|
|
127
|
+
{ label: "Presentation", value: "Not airtight" },
|
|
113
128
|
],
|
|
114
129
|
policies: [
|
|
115
|
-
{ tone: "
|
|
130
|
+
{ tone: "info", title: "Processing decides the chapter", text: "Raw frozen sits in Ch. 03; cooked / breaded / marinated moves to Ch. 16. The 20-point MFN gap makes the evidence of processing decisive." },
|
|
116
131
|
],
|
|
117
132
|
sources: [
|
|
118
|
-
{ id: "s1", label: "Tariff schedule 2026", kind: "knowledge", detail: "Ch.
|
|
119
|
-
{ id: "s2", label: "Heading
|
|
133
|
+
{ id: "s1", label: "Tariff schedule 2026", kind: "knowledge", detail: "Ch. 16" },
|
|
134
|
+
{ id: "s2", label: "Heading 1605.21", kind: "knowledge" },
|
|
120
135
|
],
|
|
121
136
|
},
|
|
122
137
|
];
|
|
@@ -131,14 +146,14 @@ const SCRIPT: ScriptStep[] = [
|
|
|
131
146
|
];
|
|
132
147
|
|
|
133
148
|
export function TplLookup() {
|
|
134
|
-
const [activeId, setActiveId] = useState(
|
|
149
|
+
const [activeId, setActiveId] = useState(CANDIDATES.find((c) => c.recommended)?.id ?? CANDIDATES[0].id);
|
|
135
150
|
const [phase, setPhase] = useState<"running" | "ready">("ready");
|
|
136
151
|
const [revealed, setRevealed] = useState(SCRIPT.length);
|
|
137
152
|
const [prompt, setPrompt] = useState("");
|
|
138
153
|
const [runKey, setRunKey] = useState(0);
|
|
139
154
|
const timer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
140
155
|
|
|
141
|
-
const active =
|
|
156
|
+
const active = CANDIDATES.find((c) => c.id === activeId) ?? CANDIDATES[0];
|
|
142
157
|
|
|
143
158
|
useEffect(() => {
|
|
144
159
|
if (runKey === 0) return;
|
|
@@ -159,16 +174,11 @@ export function TplLookup() {
|
|
|
159
174
|
}, [runKey]);
|
|
160
175
|
|
|
161
176
|
const ask = (text: string) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
(v.id === "v-batt" && (q.includes("batter") || q.includes("lithium") || q.includes("cell"))) ||
|
|
168
|
-
(v.id === "v-shrimp" && (q.includes("shrimp") || q.includes("prawn") || q.includes("frozen")))
|
|
169
|
-
);
|
|
170
|
-
}) ?? VERDICTS[0];
|
|
171
|
-
setActiveId(hit.id);
|
|
177
|
+
// Refining the description re-ranks: cooked → prepared (Ch. 16), bulk → the
|
|
178
|
+
// residual frozen line, else the recommended retail-pack code.
|
|
179
|
+
const q = text.toLowerCase();
|
|
180
|
+
const hit = /cook|prepar|breaded|marinat/.test(q) ? "c3" : /bulk|cater/.test(q) ? "c2" : "c1";
|
|
181
|
+
setActiveId(hit);
|
|
172
182
|
setPrompt("");
|
|
173
183
|
setRunKey((k) => k + 1);
|
|
174
184
|
};
|
|
@@ -185,35 +195,38 @@ export function TplLookup() {
|
|
|
185
195
|
<View style={{ width: 380, backgroundColor: colors.white, borderRightWidth: 1, borderRightColor: colors.zinc[200] }}>
|
|
186
196
|
<View style={{ paddingHorizontal: 20, paddingTop: 18, paddingBottom: 12, gap: 2 }}>
|
|
187
197
|
<Text size="md" weight="semibold">HS classification</Text>
|
|
188
|
-
<Text size="sm" color="muted">Describe the goods — the agent
|
|
198
|
+
<Text size="sm" color="muted">Describe the goods — the agent ranks the matching codes; pick one to see its duty</Text>
|
|
189
199
|
</View>
|
|
190
200
|
<Divider />
|
|
191
201
|
|
|
192
202
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, gap: 16 }}>
|
|
193
203
|
{/* current run */}
|
|
194
|
-
<AgentRun title=
|
|
204
|
+
<AgentRun title="Classifying the goods" steps={steps} state={streaming ? "streaming" : "done"} />
|
|
195
205
|
|
|
196
206
|
<View style={{ gap: 8 }}>
|
|
197
|
-
<Text size="xs" color="muted" weight="medium">
|
|
198
|
-
|
|
207
|
+
<Text size="xs" color="muted" weight="medium">Nearest matches</Text>
|
|
208
|
+
<Text size="xs" color="muted" numberOfLines={2}>{QUERY}</Text>
|
|
209
|
+
{CANDIDATES.map((c) => (
|
|
199
210
|
<PressableHighlight
|
|
200
|
-
key={
|
|
201
|
-
onPress={() => { setActiveId(
|
|
202
|
-
accessibilityLabel={`Show ${
|
|
211
|
+
key={c.id}
|
|
212
|
+
onPress={() => { setActiveId(c.id); setPhase("ready"); setRevealed(SCRIPT.length); }}
|
|
213
|
+
accessibilityLabel={`Show ${c.code} — ${c.label}`}
|
|
203
214
|
style={{
|
|
204
215
|
padding: 10,
|
|
205
216
|
borderRadius: 10,
|
|
206
217
|
borderWidth: 1,
|
|
207
|
-
borderColor:
|
|
208
|
-
backgroundColor:
|
|
209
|
-
gap:
|
|
218
|
+
borderColor: c.id === activeId ? colors.zinc[900] : colors.zinc[200],
|
|
219
|
+
backgroundColor: c.id === activeId ? colors.zinc[50] : colors.white,
|
|
220
|
+
gap: 4,
|
|
210
221
|
}}
|
|
211
222
|
>
|
|
212
|
-
<Text size="sm" weight="medium" numberOfLines={1}>{v.query}</Text>
|
|
213
223
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
214
|
-
<Text size="
|
|
215
|
-
<
|
|
224
|
+
<Text size="sm" weight="semibold" tabular numberOfLines={1}>{c.code}</Text>
|
|
225
|
+
{c.recommended ? <Badge color="blue" label="Recommended" /> : null}
|
|
226
|
+
<View style={{ flex: 1 }} />
|
|
227
|
+
<Confidence level={c.confidence} />
|
|
216
228
|
</View>
|
|
229
|
+
<Text size="xs" color="muted" numberOfLines={1}>{c.label}</Text>
|
|
217
230
|
</PressableHighlight>
|
|
218
231
|
))}
|
|
219
232
|
</View>
|