@lotics/ui 4.4.0 → 4.5.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 +98 -28
- package/examples/tpl_assistant.tsx +6 -6
- package/examples/tpl_briefing.tsx +121 -0
- package/examples/tpl_compare.tsx +133 -0
- package/examples/tpl_crosscheck.tsx +120 -0
- package/examples/tpl_dieline.tsx +218 -55
- package/examples/tpl_draft.tsx +7 -9
- package/examples/tpl_extract.tsx +91 -92
- package/examples/tpl_lookup.tsx +288 -0
- package/examples/tpl_match.tsx +123 -0
- package/examples/tpl_triage.tsx +112 -0
- package/package.json +9 -2
- package/src/agent_progress.tsx +35 -23
- package/src/agent_run.tsx +60 -80
- package/src/change_review.tsx +190 -110
- package/src/choice_list.tsx +63 -0
- package/src/clarify.tsx +19 -39
- package/src/confidence.tsx +30 -8
- package/src/discrepancy.tsx +114 -0
- package/src/finding.tsx +104 -0
- package/src/match_row.tsx +133 -0
- package/src/prompt_field.tsx +47 -89
- package/src/record_review.tsx +149 -0
- package/src/scored_option.tsx +139 -0
- package/src/sources.tsx +38 -21
- package/src/spec_list.tsx +81 -0
- package/src/suggestion.tsx +35 -45
- package/src/triage_row.tsx +99 -0
- package/src/assisted_field.tsx +0 -93
package/AGENTS.md
CHANGED
|
@@ -78,9 +78,16 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
78
78
|
- **AI surfaces** — `PromptField` (the NL command that triggers agent work — NOT a chat),
|
|
79
79
|
`AgentRun` (the live streaming work feed) + `AgentProgress` (its compact, floating, expandable
|
|
80
80
|
form — a composer's "working" state), `Suggestion` (review-for-approval of a proposal)
|
|
81
|
-
+ `Confidence`, `
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
+ `Confidence`, `RecordReview` (an extracted record reviewed + edited before save — fields
|
|
82
|
+
click-to-edit, Confirm collapses it), `ChangeReview` (before→after for an agent-proposed edit —
|
|
83
|
+
whole-set OR 1-by-1), `Clarify` (the agent asks back — selectable `ChoiceList` options),
|
|
84
|
+
`Sources` (provenance chips for AI output). For STRUCTURED AI output the four review
|
|
85
|
+
surfaces above don't cover: `SpecList` (the "exact breakdown" — a labeled-value spec sheet, the
|
|
86
|
+
panel of an answer), `MatchRow` (a two-sided AI pairing — source ↔ proposed match + confidence),
|
|
87
|
+
`Finding` (one ranked briefing/audit insight — severity + metric + sources + action), `Discrepancy`
|
|
88
|
+
(a field whose value disagrees across sources — pick the truth), `TriageRow` (an incoming item the
|
|
89
|
+
agent classified + routed), `ScoredOption` (an AI-ranked candidate — score + rationale + specs).
|
|
90
|
+
See §AI workflows.
|
|
84
91
|
|
|
85
92
|
---
|
|
86
93
|
|
|
@@ -218,7 +225,8 @@ Compose the surfaces as a loop, and reach for the right one by job:
|
|
|
218
225
|
- **Command** — `PromptField`, NOT a chat composer. A prompt TRIGGERS agent work (Enter sends);
|
|
219
226
|
while it runs the field locks and the send spins. The imperative "do this" surface.
|
|
220
227
|
- **Show the work** — `AgentRun`: a live feed of the agent's steps (reasoning, tool calls) on a
|
|
221
|
-
spine, the running node pulsing
|
|
228
|
+
spine, the running node a pulsing ink dot (a done step settles to a hollow ring). Transparent
|
|
229
|
+
work, NEVER a bare spinner — the
|
|
222
230
|
intelligence is legible. On a canvas/composer app reach for `AgentProgress` — `AgentRun`
|
|
223
231
|
collapsed into a floating pill (avatar + current step) that EXPANDS on press; the composer
|
|
224
232
|
morphs into it while running, and reveals again when done.
|
|
@@ -227,9 +235,10 @@ Compose the surfaces as a loop, and reach for the right one by job:
|
|
|
227
235
|
For a shortlist, stack ranked `Suggestion`s (highest confidence first). `ChangeReview` also has a
|
|
228
236
|
**1-by-1** mode — pass `onAcceptItem`/`onRejectItem` and each suggested change gets its own ✓/✕,
|
|
229
237
|
Apply committing only the accepted set (the chat copilot). Nothing auto-applies.
|
|
230
|
-
- **
|
|
231
|
-
|
|
232
|
-
|
|
238
|
+
- **Review an extracted record** — `RecordReview`: one record the agent pulled from a document, its
|
|
239
|
+
fields click-to-edit (`InlineTextInput`), with a confidence + Confirm / Remove; Confirm collapses
|
|
240
|
+
it to a checklist row (Undo to re-open). Stack several under a "Save all" to capture a whole
|
|
241
|
+
document → table (see `tpl_extract`). Nothing writes until the human confirms + saves.
|
|
233
242
|
- **Ask back** — `Clarify`: when the agent is unsure, it asks a question with quick-reply options
|
|
234
243
|
and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run.
|
|
235
244
|
- **Provenance** — `Sources`: openable chips saying where the output came FROM (records, a
|
|
@@ -240,22 +249,68 @@ Compose the surfaces as a loop, and reach for the right one by job:
|
|
|
240
249
|
"make it a bit less", but it's a run LOG, not a conversation transcript — that's why it's not a
|
|
241
250
|
chat.)
|
|
242
251
|
|
|
243
|
-
|
|
244
|
-
single pass, or generated prose:
|
|
245
|
-
- **Canvas** (`tpl_dieline`) — the page IS the design
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
Ten shapes prove the range. The first four PRODUCE — pick by whether the work is a living artifact,
|
|
253
|
+
a conversation, a single pass, or generated prose:
|
|
254
|
+
- **Canvas** (`tpl_dieline`) — the page IS the design on a pannable/zoomable surface: it stays
|
|
255
|
+
CENTRED at every zoom (a floating zoom pill bottom-left), the FLOATING composer at the bottom
|
|
256
|
+
morphs into `AgentProgress` while running, and a pinned PARAMS PANEL (the `RecordReview` field-card
|
|
257
|
+
in live-edit mode, carrying the single Download; minimizes to a pill) floats centre-right. Panel, zoom pill and composer
|
|
258
|
+
float ON TOP — they never shift the design. (Floating layers use a `pointerEvents:"none"` wrapper
|
|
259
|
+
with the interactive child set `"auto"`; RN-Web ignores `"box-none"` in style, so a full-width
|
|
260
|
+
wrapper would otherwise eat clicks on the canvas behind it.) Change the design by prompt ("5 mm
|
|
261
|
+
taller") OR by editing a param directly — either re-flows it in place. For a design/document the
|
|
262
|
+
user shapes over time.
|
|
248
263
|
- **Chat** (`tpl_assistant`) — a conversation where the agent's answer carries suggested edits the
|
|
249
264
|
user approves/rejects **1-by-1** (`ChangeReview` per-item), applying only the accepted set. An AI
|
|
250
265
|
copilot that proposes changes to a record and never commits on its own.
|
|
251
|
-
- **One-shot** (`tpl_extract`) —
|
|
252
|
-
|
|
266
|
+
- **One-shot** (`tpl_extract`, `RecordReview`) — read an image → the agent extracts the records →
|
|
267
|
+
review each (`RecordReview`: fields click-to-edit, Confirm collapses it to a checklist row) →
|
|
268
|
+
Save all to the table. A data-capture pass; nothing stored until confirmed.
|
|
253
269
|
- **Draft** (`tpl_draft`) — the agent writes a first draft from context; the OUTPUT itself streams
|
|
254
270
|
(the text fills in live), then becomes an editable surface the human refines before sending.
|
|
255
271
|
Tone/length steer it. For generated prose — a reply, a quote, a listing.
|
|
256
272
|
|
|
257
|
-
The
|
|
258
|
-
|
|
273
|
+
The next six output STRUCTURE the first four can't — when the answer is a panel of facts, a queue of
|
|
274
|
+
decisions, or a ranked set, don't cram it into chat prose. Each pairs a template with a primitive:
|
|
275
|
+
- **Answer desk** (`tpl_lookup`, `SpecList`) — converse on the LEFT, a pinned STRUCTURED answer on
|
|
276
|
+
the RIGHT (a verdict header + an exact breakdown + the policies + sources); a follow-up refines the
|
|
277
|
+
same panel. For look-up-and-explain: tariff/HS, fee lookup, policy Q&A, a spec/compliance desk. The
|
|
278
|
+
structured answer is the product; chat just steers. (NOT a chat with the answer buried in a bubble.)
|
|
279
|
+
- **Match** (`tpl_match`, `MatchRow`) — the agent proposes PAIRINGS with its reasoning; each row is
|
|
280
|
+
two-sided (source ↔ proposed counterpart + confidence), accept / reassign / reject, high-confidence
|
|
281
|
+
batch-accept. The AI reconciliation / dedup / correlation queue. (The deterministic cousin
|
|
282
|
+
`tpl_reconcile` has no agent reasoning — that's the tell for which to reach for.)
|
|
283
|
+
- **Briefing** (`tpl_briefing`, `Finding`) — a generated narrative over the data + a RANKED list of
|
|
284
|
+
what needs attention, each finding carrying a severity, a metric, its sources, and one action. The
|
|
285
|
+
leadership digest / anomaly scan — the agent's COMMENTARY, not a raw dashboard.
|
|
286
|
+
- **Cross-check** (`tpl_crosscheck`, `Discrepancy`) — the agent compares the documents of one
|
|
287
|
+
transaction and surfaces every field that DISAGREES; resolve each to a source of truth or flag it.
|
|
288
|
+
The audit / 3-way-match desk. Symmetric (N sources), unlike a before→after edit.
|
|
289
|
+
- **Triage** (`tpl_triage`, `TriageRow`) — an inbox the agent classified + routed; accept the call,
|
|
290
|
+
override, or dismiss, high-confidence in bulk. Leads, tickets, documents, emails.
|
|
291
|
+
- **Compare** (`tpl_compare`, `ScoredOption`) — the agent SCORES and ranks options with its rationale
|
|
292
|
+
and the key specs; the human picks one. Quotes, carriers, suppliers, plans.
|
|
293
|
+
|
|
294
|
+
The AI vocabulary has **no purple accent and no gimmick glyphs** (no sparkles) — but it is NOT
|
|
295
|
+
monochrome: **colour is used where it carries meaning, not for decoration.** What the violet sparkle
|
|
296
|
+
used to carry now reads structurally — **provenance** is an uppercase microlabel naming the artifact
|
|
297
|
+
(`PROPOSED` · `MATCH` · `MISMATCH` · `SUGGESTED EDIT` · `QUESTION`), and the agent's **reasoning** is
|
|
298
|
+
a left-ruled margin note (a hairline rule + muted text), quoted apart from the facts and the human's
|
|
299
|
+
controls — while **status/severity/diffs use functional colour** the way the rest of the kit does:
|
|
300
|
+
- **Confidence** — a 3-tick meter + word, emerald / amber / zinc by level.
|
|
301
|
+
- **Finding severity** — a coloured dot `Badge` + metric: red critical, amber warning, blue note,
|
|
302
|
+
emerald on-track (+ stacking order, most severe first).
|
|
303
|
+
- **AgentRun** — `Timeline`-style tinted-disc nodes that fade in: a running step spins **blue**, a
|
|
304
|
+
finished step is an **emerald check**, a failure a **red** alert; the header reads `Working` (blue
|
|
305
|
+
spinner) / `Done` (emerald check) / `Stopped` (red).
|
|
306
|
+
- **ChangeReview diffs** — the old value struck in **red**, the new value in **green**.
|
|
307
|
+
- **Discrepancy** — the agent's pick gets a **blue** highlight; `Mismatch` reads amber, `Resolved`
|
|
308
|
+
emerald.
|
|
309
|
+
|
|
310
|
+
Card chrome (borders, microlabels) stays neutral — colour marks the *state*, never the container.
|
|
311
|
+
The **composer keeps its icons**: `PromptField` is a single-row pill with circular attach + send
|
|
312
|
+
`Button`s; `AgentProgress` is the `WaveAvatar` pill. "No icons" was only ever about the review
|
|
313
|
+
surfaces' sparkles/severity glyphs, not functional affordances.
|
|
259
314
|
|
|
260
315
|
---
|
|
261
316
|
|
|
@@ -274,10 +329,14 @@ marks what came from the agent.
|
|
|
274
329
|
content, sections separated by ONE `Divider`. Do NOT wrap each section in its own `CardHeader`
|
|
275
330
|
(its auto-divider double-stacks hairlines around every title). A card-less full page uses
|
|
276
331
|
`SectionHeading` + `SectionHeadingTitle`. NEVER a bare eyebrow standing in for a section title.
|
|
277
|
-
· **Eyebrow** (`<Text size="xs" color="muted"
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
332
|
+
· **Eyebrow / label** (`<Text size="xs" color="muted" weight="medium">` — **sentence case, NEVER
|
|
333
|
+
`transform="uppercase"`**): a small quiet label above or beside content — an artifact tag
|
|
334
|
+
("Proposed", "Question", "Suggested edit"), a field name, a metric caption, a minor one-line
|
|
335
|
+
label. **All-caps is banned — it reads as shouting and the redundant uppercasing of every little
|
|
336
|
+
label is the #1 thing that makes a surface feel templated.** Sentence case + medium weight, full
|
|
337
|
+
stop. (A COLORED status word — a verdict like "Mismatch" / "Resolved" — keeps the same xs/medium
|
|
338
|
+
shape with a status `color`, still not uppercase.) This applies to every hand-written label; don't
|
|
339
|
+
reach for a wrapper component either, just write the `Text`.
|
|
281
340
|
· **Gate header**: a `Dialog` uses `DialogHeaderTitle`; a popover form uses `Text size="sm"
|
|
282
341
|
weight="semibold"` + an optional `xs muted` subtitle.
|
|
283
342
|
- **Time-constrained data gets a period filter** in the header band — `DateRangeFilterField`, never
|
|
@@ -401,12 +460,18 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
|
|
|
401
460
|
`tpl_timeline` (audit feed).
|
|
402
461
|
- **Planning & time** — `tpl_calendar` · `tpl_attendance` · `tpl_shifts`.
|
|
403
462
|
- **Administration** — `tpl_settings`.
|
|
404
|
-
- **AI workflows** — `tpl_dieline` (the design CANVAS: photo → stream → the dieline
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
`
|
|
408
|
-
`
|
|
409
|
-
|
|
463
|
+
- **AI workflows** — *produce*: `tpl_dieline` (the design CANVAS: photo → stream → the dieline
|
|
464
|
+
reveals CENTRED on a pannable/zoomable surface, the floating composer morphing into `AgentProgress`,
|
|
465
|
+
a pinned live-edit params panel centre-right; prompt OR edit a param to iterate) ·
|
|
466
|
+
`tpl_assistant` (a chat copilot: the agent's answer carries suggested edits approved/rejected
|
|
467
|
+
1-by-1) · `tpl_extract` (read an image → extract records → review/edit each (`RecordReview`) →
|
|
468
|
+
Save all) · `tpl_draft` (draft generation: context → the reply text streams into an editable surface
|
|
469
|
+
→ refine → send; tone/length steer it). *Structure*: `tpl_lookup` (the ANSWER desk: converse left,
|
|
470
|
+
a pinned `SpecList` verdict panel right; a follow-up refines it) · `tpl_match` (AI reconciliation:
|
|
471
|
+
`MatchRow` pairings with reasoning, accept/reassign/reject) · `tpl_briefing` (a generated narrative
|
|
472
|
+
+ a ranked `Finding` attention list) · `tpl_crosscheck` (the audit desk: `Discrepancy` cards where
|
|
473
|
+
the documents disagree) · `tpl_triage` (an inbox of `TriageRow`s the agent classified + routed) ·
|
|
474
|
+
`tpl_compare` (ranked `ScoredOption` candidates the agent scored).
|
|
410
475
|
|
|
411
476
|
---
|
|
412
477
|
|
|
@@ -437,7 +502,12 @@ spacer · stack · section_card · page_header · page_content · calendar (cale
|
|
|
437
502
|
comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
|
|
438
503
|
expandable form — a composer's working state) · prompt_field (the NL command surface + optional
|
|
439
504
|
`onAttach`, not a chat) · suggestion (review-for-approval) · confidence (calibrated high/med/low) ·
|
|
440
|
-
|
|
441
|
-
clarify (the agent asks back) ·
|
|
505
|
+
record_review (RecordReview — an extracted record reviewed + edited before save) · change_review (before→after — whole-set OR 1-by-1) ·
|
|
506
|
+
clarify (the agent asks back, via ChoiceList) · choice_list (ChoiceList — selectable answer options,
|
|
507
|
+
the agent's quick-reply surface) · sources (provenance chips, per-kind glyphs) · spec_list (SpecList — the exact
|
|
508
|
+
labeled-value breakdown, the panel of an answer) · match_row (MatchRow — a two-sided AI pairing) ·
|
|
509
|
+
finding (Finding — one ranked briefing/audit insight) · discrepancy (Discrepancy — a field that
|
|
510
|
+
disagrees across sources) · triage_row (TriageRow — an item the agent classified + routed) ·
|
|
511
|
+
scored_option (ScoredOption — an AI-ranked candidate) ·
|
|
442
512
|
format_money · format_date · colors (solid · tint · ramp · ColorName ·
|
|
443
513
|
isColorName · asColorName — coerce a stored option/status token to a ColorName, neutral fallback).
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { useRef, useState } from "react";
|
|
2
2
|
import { ScrollView, View } from "react-native";
|
|
3
|
-
import { colors
|
|
3
|
+
import { colors } from "@lotics/ui/colors";
|
|
4
4
|
import { Text } from "@lotics/ui/text";
|
|
5
|
-
import { Icon } from "@lotics/ui/icon";
|
|
6
5
|
import { DotsIndicator } from "@lotics/ui/dots_indicator";
|
|
7
6
|
import { PromptField } from "@lotics/ui/prompt_field";
|
|
8
7
|
import { ChangeReview, type ChangeReviewItem, type ChangeReviewItemStatus } from "@lotics/ui/change_review";
|
|
@@ -80,8 +79,8 @@ export function TplAssistant() {
|
|
|
80
79
|
return (
|
|
81
80
|
<View style={{ flex: 1, backgroundColor: colors.zinc[50] }}>
|
|
82
81
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.zinc[200], backgroundColor: colors.background }}>
|
|
83
|
-
<View style={{ width: 24, height: 24, borderRadius: 7, alignItems: "center", justifyContent: "center", backgroundColor:
|
|
84
|
-
<
|
|
82
|
+
<View style={{ width: 24, height: 24, borderRadius: 7, alignItems: "center", justifyContent: "center", backgroundColor: colors.zinc[100] }}>
|
|
83
|
+
<View style={{ width: 4, height: 4, borderRadius: 2, backgroundColor: colors.zinc[900] }} />
|
|
85
84
|
</View>
|
|
86
85
|
<Text size="sm" weight="semibold">Record assistant</Text>
|
|
87
86
|
</View>
|
|
@@ -100,8 +99,8 @@ export function TplAssistant() {
|
|
|
100
99
|
const active = m.changes != null && !m.applied;
|
|
101
100
|
return (
|
|
102
101
|
<View key={m.id} style={{ flexDirection: "row", gap: 10, alignItems: "flex-start" }}>
|
|
103
|
-
<View style={{ width: 26, height: 26, borderRadius: 8, alignItems: "center", justifyContent: "center", backgroundColor:
|
|
104
|
-
<
|
|
102
|
+
<View style={{ width: 26, height: 26, borderRadius: 8, alignItems: "center", justifyContent: "center", backgroundColor: colors.zinc[100], marginTop: 1 }}>
|
|
103
|
+
<View style={{ width: 4, height: 4, borderRadius: 2, backgroundColor: colors.zinc[900] }} />
|
|
105
104
|
</View>
|
|
106
105
|
<View style={{ flex: 1, gap: 10 }}>
|
|
107
106
|
{m.typing ? (
|
|
@@ -115,6 +114,7 @@ export function TplAssistant() {
|
|
|
115
114
|
applyLabel="Apply accepted"
|
|
116
115
|
onAcceptItem={active ? (i) => decide(m.id, i, "accepted") : undefined}
|
|
117
116
|
onRejectItem={active ? (i) => decide(m.id, i, "rejected") : undefined}
|
|
117
|
+
onUndoItem={active ? (i) => setDecisions((d) => { const n = { ...d }; delete n[`${m.id}:${i}`]; return n; }) : undefined}
|
|
118
118
|
onApply={active ? () => applyMsg(m.id) : undefined}
|
|
119
119
|
onDiscard={active ? () => setMessages((ms) => ms.map((x) => (x.id === m.id ? { ...x, applied: true } : x))) : undefined}
|
|
120
120
|
/>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ScrollView, View } from "react-native";
|
|
3
|
+
import { Text } from "@lotics/ui/text";
|
|
4
|
+
import { colors } from "@lotics/ui/colors";
|
|
5
|
+
import { KPIStrip } from "@lotics/ui/kpi_strip";
|
|
6
|
+
import { SegmentedControl } from "@lotics/ui/segmented_control";
|
|
7
|
+
import { Sources } from "@lotics/ui/sources";
|
|
8
|
+
import { Finding, type FindingSeverity } from "@lotics/ui/finding";
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Template · Briefing — the agent reads the period's data and writes a short
|
|
12
|
+
// narrative, then a RANKED list of what needs attention. Each finding carries a
|
|
13
|
+
// severity, a headline metric, the sources it rests on, and the one action it
|
|
14
|
+
// suggests. Unlike a dashboard (raw charts), this is the agent's COMMENTARY:
|
|
15
|
+
// what changed, what's at risk, what to do. The leadership digest / daily ops
|
|
16
|
+
// brief / anomaly scan. The narrative explains; the findings are the worklist.
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface F {
|
|
20
|
+
severity: FindingSeverity;
|
|
21
|
+
title: string;
|
|
22
|
+
detail: string;
|
|
23
|
+
metric: string;
|
|
24
|
+
metricCaption: string;
|
|
25
|
+
action: string;
|
|
26
|
+
sources: { id: string; label: string; kind: "record" | "table" | "document" }[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const NARRATIVE: Record<"today" | "week", string> = {
|
|
30
|
+
today:
|
|
31
|
+
"Volume held steady at 41 active shipments. Three reefer containers to Hamburg are inside the demurrage window and need a pickup booked today. Cash collection is lagging — five invoices crossed 30 days overdue. On-time delivery improved on the back of the new Cát Lái slotting.",
|
|
32
|
+
week:
|
|
33
|
+
"A solid week: 268 shipments moved, on-time delivery up 6 points after the Cát Lái slotting change. Two ocean lanes slipped below the 12% margin target on bunker surcharges. Receivables remain the soft spot — ₫120M is now past 30 days, concentrated in two accounts.",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const FINDINGS: Record<"today" | "week", F[]> = {
|
|
37
|
+
today: [
|
|
38
|
+
{ severity: "critical", title: "3 reefer containers at demurrage risk", detail: "Hamburg shipments HBL-4471/4472/4480 hit free-time expiry at 18:00 today; no pickup is booked.", metric: "₫48M", metricCaption: "exposure", action: "Open the shipments", sources: [{ id: "a1", label: "SHIP-4471", kind: "record" }, { id: "a2", label: "Demurrage tracker", kind: "table" }] },
|
|
39
|
+
{ severity: "warning", title: "5 invoices overdue past 30 days", detail: "Two accounts — Crestline and Meridian — make up most of the balance. No promise-to-pay logged this week.", metric: "₫120M", metricCaption: "overdue", action: "Open receivables", sources: [{ id: "a3", label: "Aged receivables", kind: "table" }] },
|
|
40
|
+
{ severity: "warning", title: "Margin on the Hamburg lane below target", detail: "Bunker surcharge rose 9%; the lane is at 9.8% gross vs the 12% floor.", metric: "−2.2 pts", metricCaption: "vs target", action: "Review lane pricing", sources: [{ id: "a4", label: "Lane P&L", kind: "table" }] },
|
|
41
|
+
{ severity: "positive", title: "On-time delivery improving", detail: "The Cát Lái slotting change lifted on-time pickups for the third day running.", metric: "+6 pts", metricCaption: "3-day", action: "See the trend", sources: [{ id: "a5", label: "OTD report", kind: "document" }] },
|
|
42
|
+
],
|
|
43
|
+
week: [
|
|
44
|
+
{ severity: "critical", title: "Receivables concentration risk", detail: "₫120M past 30 days sits in two accounts; one is also near its credit limit.", metric: "2", metricCaption: "accounts", action: "Open receivables", sources: [{ id: "b1", label: "Aged receivables", kind: "table" }] },
|
|
45
|
+
{ severity: "warning", title: "Two ocean lanes below margin target", detail: "Hamburg and Rotterdam slipped under 12% on bunker surcharges sustained all week.", metric: "2", metricCaption: "lanes", action: "Review lane pricing", sources: [{ id: "b2", label: "Lane P&L", kind: "table" }] },
|
|
46
|
+
{ severity: "positive", title: "On-time delivery up 6 points", detail: "The slotting change held all week — the strongest OTD in two months.", metric: "+6 pts", metricCaption: "this week", action: "See the trend", sources: [{ id: "b3", label: "OTD report", kind: "document" }] },
|
|
47
|
+
{ severity: "info", title: "RFQ volume rising", detail: "12 new quote requests this week, ahead of the trailing average — capacity planning should get ahead of it.", metric: "12", metricCaption: "RFQs", action: "Open the pipeline", sources: [{ id: "b4", label: "Quote pipeline", kind: "table" }] },
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function TplBriefing() {
|
|
52
|
+
const [period, setPeriod] = useState<"today" | "week">("today");
|
|
53
|
+
const findings = FINDINGS[period];
|
|
54
|
+
const attention = findings.filter((f) => f.severity === "critical" || f.severity === "warning").length;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
|
|
58
|
+
<View style={{ width: "100%", maxWidth: 860, alignSelf: "center", gap: 16 }}>
|
|
59
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
|
|
60
|
+
<View style={{ flex: 1, gap: 2 }}>
|
|
61
|
+
<Text size="xl" weight="semibold">Operations briefing</Text>
|
|
62
|
+
<Text size="sm" color="muted">What the agent sees in the numbers — and what needs a decision</Text>
|
|
63
|
+
</View>
|
|
64
|
+
<SegmentedControl
|
|
65
|
+
accessibilityLabel="Period"
|
|
66
|
+
options={[
|
|
67
|
+
{ label: "Today", value: "today" },
|
|
68
|
+
{ label: "This week", value: "week" },
|
|
69
|
+
]}
|
|
70
|
+
value={period}
|
|
71
|
+
onValueChange={setPeriod}
|
|
72
|
+
/>
|
|
73
|
+
</View>
|
|
74
|
+
|
|
75
|
+
{/* the generated narrative */}
|
|
76
|
+
<View style={{ borderWidth: 1, borderColor: colors.zinc[200], backgroundColor: colors.white, borderRadius: 14, padding: 18, gap: 12 }}>
|
|
77
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
78
|
+
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Summary</Text>
|
|
79
|
+
<Text size="xs" color="muted">{period === "today" ? "as of 14:20" : "week to date"}</Text>
|
|
80
|
+
</View>
|
|
81
|
+
<Text size="md" style={{ lineHeight: 22 }}>{NARRATIVE[period]}</Text>
|
|
82
|
+
<Sources
|
|
83
|
+
label="Read from"
|
|
84
|
+
onOpen={() => {}}
|
|
85
|
+
sources={[
|
|
86
|
+
{ id: "n1", label: "Shipments", kind: "table" },
|
|
87
|
+
{ id: "n2", label: "Aged receivables", kind: "table" },
|
|
88
|
+
{ id: "n3", label: "Lane P&L", kind: "table" },
|
|
89
|
+
]}
|
|
90
|
+
/>
|
|
91
|
+
</View>
|
|
92
|
+
|
|
93
|
+
<KPIStrip
|
|
94
|
+
items={[
|
|
95
|
+
{ label: "Active shipments", value: period === "today" ? 41 : 268, format: "number", trend: period === "today" ? 0 : 4 },
|
|
96
|
+
{ label: "On-time delivery", value: "94%", trend: 6 },
|
|
97
|
+
{ label: "Overdue receivables", value: 120_000_000, format: "currency", compact: true, tone: "danger" },
|
|
98
|
+
{ label: "Needs attention", value: attention, format: "number", tone: attention > 0 ? "warning" : "default" },
|
|
99
|
+
]}
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<View style={{ gap: 10 }}>
|
|
103
|
+
<Text size="sm" weight="semibold">Needs attention</Text>
|
|
104
|
+
{findings.map((f, i) => (
|
|
105
|
+
<Finding
|
|
106
|
+
key={i}
|
|
107
|
+
severity={f.severity}
|
|
108
|
+
title={f.title}
|
|
109
|
+
detail={f.detail}
|
|
110
|
+
metric={f.metric}
|
|
111
|
+
metricCaption={f.metricCaption}
|
|
112
|
+
sources={f.sources}
|
|
113
|
+
onOpenSource={() => {}}
|
|
114
|
+
action={{ label: f.action, onPress: () => {} }}
|
|
115
|
+
/>
|
|
116
|
+
))}
|
|
117
|
+
</View>
|
|
118
|
+
</View>
|
|
119
|
+
</ScrollView>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ScrollView, View } from "react-native";
|
|
3
|
+
import { Text } from "@lotics/ui/text";
|
|
4
|
+
import { colors } from "@lotics/ui/colors";
|
|
5
|
+
import { Callout, CalloutText, CalloutTitle } from "@lotics/ui/callout";
|
|
6
|
+
import { ScoredOption } from "@lotics/ui/scored_option";
|
|
7
|
+
import type { SpecRow } from "@lotics/ui/spec_list";
|
|
8
|
+
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Template · Compare — the agent SCORES and ranks the options against the
|
|
11
|
+
// criteria, shows its reasoning and the key specs, and the human picks one. The
|
|
12
|
+
// agent does the legwork (gather, normalise, score); the choice stays the
|
|
13
|
+
// human's. Quotes, carriers, suppliers, plans, routes. The recommended option
|
|
14
|
+
// wears a RECOMMENDED label; selecting one settles the shortlist to the decision.
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
interface O {
|
|
18
|
+
id: string;
|
|
19
|
+
rank: number;
|
|
20
|
+
title: string;
|
|
21
|
+
subtitle: string;
|
|
22
|
+
score: number;
|
|
23
|
+
rationale: string;
|
|
24
|
+
specs: SpecRow[];
|
|
25
|
+
recommended?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const OPTIONS: O[] = [
|
|
29
|
+
{
|
|
30
|
+
id: "o1",
|
|
31
|
+
rank: 1,
|
|
32
|
+
title: "Maersk — Cát Lái → Hamburg",
|
|
33
|
+
subtitle: "Direct, weekly service · reefer guaranteed",
|
|
34
|
+
score: 0.92,
|
|
35
|
+
rationale: "Best balance: only 4% above the cheapest, but the most reliable schedule and guaranteed reefer plugs — lowest demurrage risk for time-sensitive cargo.",
|
|
36
|
+
recommended: true,
|
|
37
|
+
specs: [
|
|
38
|
+
{ label: "All-in rate", value: "$2,840" },
|
|
39
|
+
{ label: "Transit", value: "28 days" },
|
|
40
|
+
{ label: "Schedule reliability", value: "94%" },
|
|
41
|
+
{ label: "Free time", value: "14 days" },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "o2",
|
|
46
|
+
rank: 2,
|
|
47
|
+
title: "CMA CGM — Cát Lái → Hamburg",
|
|
48
|
+
subtitle: "1 transshipment at Singapore",
|
|
49
|
+
score: 0.78,
|
|
50
|
+
rationale: "Cheapest of the four, but the Singapore transshipment adds three days and a reliability hit — workable for non-urgent cargo.",
|
|
51
|
+
specs: [
|
|
52
|
+
{ label: "All-in rate", value: "$2,730" },
|
|
53
|
+
{ label: "Transit", value: "31 days" },
|
|
54
|
+
{ label: "Schedule reliability", value: "86%" },
|
|
55
|
+
{ label: "Free time", value: "10 days" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "o3",
|
|
60
|
+
rank: 3,
|
|
61
|
+
title: "Hapag-Lloyd — Cát Lái → Hamburg",
|
|
62
|
+
subtitle: "Direct, premium service",
|
|
63
|
+
score: 0.64,
|
|
64
|
+
rationale: "Fastest transit and strong reliability, but the premium rate is hard to justify unless the customer pays for speed.",
|
|
65
|
+
specs: [
|
|
66
|
+
{ label: "All-in rate", value: "$3,210" },
|
|
67
|
+
{ label: "Transit", value: "26 days" },
|
|
68
|
+
{ label: "Schedule reliability", value: "92%" },
|
|
69
|
+
{ label: "Free time", value: "10 days" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "o4",
|
|
74
|
+
rank: 4,
|
|
75
|
+
title: "ONE — Cát Lái → Hamburg",
|
|
76
|
+
subtitle: "2 transshipments · no reefer guarantee",
|
|
77
|
+
score: 0.41,
|
|
78
|
+
rationale: "Lowest score: two transshipments and no guaranteed reefer plug make it unsuitable for frozen cargo, despite a mid-range rate.",
|
|
79
|
+
specs: [
|
|
80
|
+
{ label: "All-in rate", value: "$2,910" },
|
|
81
|
+
{ label: "Transit", value: "34 days" },
|
|
82
|
+
{ label: "Schedule reliability", value: "79%" },
|
|
83
|
+
{ label: "Free time", value: "7 days" },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
export function TplCompare() {
|
|
89
|
+
const [chosen, setChosen] = useState<string | null>(null);
|
|
90
|
+
const pick = OPTIONS.find((o) => o.id === chosen) ?? null;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
|
|
94
|
+
<View style={{ width: "100%", maxWidth: 720, alignSelf: "center", gap: 16 }}>
|
|
95
|
+
<View style={{ gap: 2 }}>
|
|
96
|
+
<Text size="xl" weight="semibold">Freight options</Text>
|
|
97
|
+
<Text size="sm" color="muted">Ocean · Cát Lái → Hamburg · 2×40HC reefer · frozen shrimp</Text>
|
|
98
|
+
</View>
|
|
99
|
+
|
|
100
|
+
<View style={{ borderLeftWidth: 2, borderLeftColor: colors.zinc[300], paddingLeft: 12 }}>
|
|
101
|
+
<Text size="sm" color="muted">
|
|
102
|
+
The agent scored four quotes against cost, transit time and schedule reliability, weighted for time-sensitive reefer cargo.
|
|
103
|
+
</Text>
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
{pick ? (
|
|
107
|
+
<Callout tone="neutral">
|
|
108
|
+
<CalloutTitle>Chosen · {pick.title}</CalloutTitle>
|
|
109
|
+
<CalloutText>{pick.specs[0].label} {String(pick.specs[0].value)} · {String(pick.specs[1].value)} transit. Booking can proceed on this option.</CalloutText>
|
|
110
|
+
</Callout>
|
|
111
|
+
) : null}
|
|
112
|
+
|
|
113
|
+
<View style={{ gap: 12 }}>
|
|
114
|
+
{OPTIONS.map((o) => (
|
|
115
|
+
<ScoredOption
|
|
116
|
+
key={o.id}
|
|
117
|
+
rank={o.rank}
|
|
118
|
+
title={o.title}
|
|
119
|
+
subtitle={o.subtitle}
|
|
120
|
+
score={o.score}
|
|
121
|
+
rationale={o.rationale}
|
|
122
|
+
specs={o.specs}
|
|
123
|
+
recommended={o.recommended}
|
|
124
|
+
selected={chosen === o.id}
|
|
125
|
+
selectLabel="Choose this option"
|
|
126
|
+
onSelect={() => setChosen((c) => (c === o.id ? null : o.id))}
|
|
127
|
+
/>
|
|
128
|
+
))}
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
</ScrollView>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ScrollView, View } from "react-native";
|
|
3
|
+
import { Text } from "@lotics/ui/text";
|
|
4
|
+
import { colors } from "@lotics/ui/colors";
|
|
5
|
+
import { KPIStrip } from "@lotics/ui/kpi_strip";
|
|
6
|
+
import { Callout, CalloutText, CalloutTitle } from "@lotics/ui/callout";
|
|
7
|
+
import { Discrepancy, type DiscrepancyValue } from "@lotics/ui/discrepancy";
|
|
8
|
+
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Template · Cross-check — the agent compares the documents of one transaction
|
|
11
|
+
// and surfaces every field whose value DISAGREES across sources. Each card lays
|
|
12
|
+
// the conflicting values side by side (with where each came from), marks the one
|
|
13
|
+
// the agent believes, and the human resolves to a truth or flags it. The audit /
|
|
14
|
+
// 3-way-match / contract-vs-invoice desk. Symmetric (N sources disagree), unlike
|
|
15
|
+
// a before→after edit. The agreeing fields stay out of the way — only conflicts
|
|
16
|
+
// need a decision.
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface D {
|
|
20
|
+
id: string;
|
|
21
|
+
field: string;
|
|
22
|
+
values: DiscrepancyValue[];
|
|
23
|
+
note: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SEED: D[] = [
|
|
27
|
+
{
|
|
28
|
+
id: "d1",
|
|
29
|
+
field: "Total cartons",
|
|
30
|
+
values: [
|
|
31
|
+
{ source: "Shipping instruction", value: "525" },
|
|
32
|
+
{ source: "Commercial invoice", value: "520", recommended: true },
|
|
33
|
+
{ source: "Packing list", value: "520" },
|
|
34
|
+
],
|
|
35
|
+
note: "Two of three documents agree at 520; the SI looks stale — it predates the final load.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "d2",
|
|
39
|
+
field: "Net weight",
|
|
40
|
+
values: [
|
|
41
|
+
{ source: "Commercial invoice", value: "11,980 kg" },
|
|
42
|
+
{ source: "Packing list", value: "12,050 kg", recommended: true },
|
|
43
|
+
],
|
|
44
|
+
note: "The packing list carries the measured weight; the CI rounded for the invoice.",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "d3",
|
|
48
|
+
field: "Incoterm",
|
|
49
|
+
values: [
|
|
50
|
+
{ source: "Shipping instruction", value: "FOB" },
|
|
51
|
+
{ source: "Commercial invoice", value: "CIF", recommended: true },
|
|
52
|
+
],
|
|
53
|
+
note: "The commercial invoice governs the sale terms — CIF matches the freight prepaid on the bill of lading.",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "d4",
|
|
57
|
+
field: "HS code",
|
|
58
|
+
values: [
|
|
59
|
+
{ source: "Commercial invoice", value: "0306.17", recommended: true },
|
|
60
|
+
{ source: "Customs draft", value: "0306.16" },
|
|
61
|
+
],
|
|
62
|
+
note: "0306.16 is cold-water shrimp; the goods are warm-water Penaeidae → 0306.17. The CI is right — fix the draft.",
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const CHECKED = 22;
|
|
67
|
+
|
|
68
|
+
export function TplCrosscheck() {
|
|
69
|
+
const [res, setRes] = useState<Record<string, { idx?: number; flagged?: boolean }>>({});
|
|
70
|
+
|
|
71
|
+
const resolvedCount = Object.values(res).filter((r) => r.idx != null || r.flagged).length;
|
|
72
|
+
const openCount = SEED.length - resolvedCount;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
|
|
76
|
+
<View style={{ width: "100%", maxWidth: 760, alignSelf: "center", gap: 16 }}>
|
|
77
|
+
<View style={{ gap: 2 }}>
|
|
78
|
+
<Text size="xl" weight="semibold">Document cross-check</Text>
|
|
79
|
+
<Text size="sm" color="muted">Shipment HBL-4471 · Shipping instruction · Commercial invoice · Packing list · Customs draft</Text>
|
|
80
|
+
</View>
|
|
81
|
+
|
|
82
|
+
<KPIStrip
|
|
83
|
+
items={[
|
|
84
|
+
{ label: "Fields checked", value: CHECKED, format: "number" },
|
|
85
|
+
{ label: "In agreement", value: CHECKED - SEED.length, format: "number" },
|
|
86
|
+
{ label: "Disagree", value: openCount, format: "number", tone: openCount > 0 ? "danger" : "default", info: "Fields whose value differs across the documents — each needs a resolution before the set can be filed." },
|
|
87
|
+
{ label: "Resolved", value: resolvedCount, format: "number" },
|
|
88
|
+
]}
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
{openCount > 0 ? (
|
|
92
|
+
<Callout tone="warning">
|
|
93
|
+
<CalloutTitle>{openCount} of {CHECKED} fields disagree across the documents</CalloutTitle>
|
|
94
|
+
<CalloutText>The agent reconciled the other {CHECKED - SEED.length}. Resolve each conflict to a source of truth, or flag it for the desk.</CalloutText>
|
|
95
|
+
</Callout>
|
|
96
|
+
) : (
|
|
97
|
+
<Callout tone="success">
|
|
98
|
+
<CalloutTitle>All conflicts resolved</CalloutTitle>
|
|
99
|
+
<CalloutText>Every field now has a single source of truth — the document set is ready to file.</CalloutText>
|
|
100
|
+
</Callout>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<View style={{ gap: 10 }}>
|
|
104
|
+
{SEED.map((d) => (
|
|
105
|
+
<Discrepancy
|
|
106
|
+
key={d.id}
|
|
107
|
+
field={d.field}
|
|
108
|
+
values={d.values}
|
|
109
|
+
note={d.note}
|
|
110
|
+
resolvedIndex={res[d.id]?.idx}
|
|
111
|
+
flagged={res[d.id]?.flagged}
|
|
112
|
+
onResolve={(idx) => setRes((p) => ({ ...p, [d.id]: { idx } }))}
|
|
113
|
+
onFlag={() => setRes((p) => ({ ...p, [d.id]: { flagged: true } }))}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
</View>
|
|
117
|
+
</View>
|
|
118
|
+
</ScrollView>
|
|
119
|
+
);
|
|
120
|
+
}
|