@lotics/ui 7.19.3 → 8.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.
@@ -1,193 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { ScrollView, View } from "react-native";
3
- import { colors } from "@lotics/ui/colors";
4
- import { Text } from "@lotics/ui/text";
5
- import { Button } from "@lotics/ui/button";
6
- import { Card, CardBody, CardHeader, CardHeaderTitle } from "@lotics/ui/card";
7
- import { FileDropzone } from "@lotics/ui/file_dropzone";
8
- import { AgentRun, type AgentRunStep, type AgentRunItem } from "@lotics/ui/agent_run";
9
- import { ReviewCard } from "@lotics/ui/review_card";
10
- import { RecordFields, type RecordField } from "@lotics/ui/record_fields";
11
- import { Sources } from "@lotics/ui/sources";
12
- import { CompletionState } from "@lotics/ui/completion_state";
13
- import { type ConfidenceLevel } from "@lotics/ui/confidence";
14
-
15
- // ─────────────────────────────────────────────────────────────────────────────
16
- // Template · AI extract & confirm — read an image, then review each extracted
17
- // RECORD before it's added to a table. Drop a photo → the agent extracts the
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),
20
- // Remove skips it → Save all writes the confirmed lines at once. A one-shot
21
- // capture pass: nothing is stored until the human has reviewed it. All mock.
22
- // ─────────────────────────────────────────────────────────────────────────────
23
-
24
- type ScriptStep = Omit<AgentRunStep, "status">;
25
- type Phase = "intake" | "extracting" | "review" | "done";
26
- type Status = "pending" | "confirmed" | "removed";
27
-
28
- const EXTRACT: ScriptStep[] = [
29
- { id: "x1", label: "Reading the packing list", detail: "A photo of a 3-line shipping manifest." },
30
- { id: "x2", label: "detect_table", kind: "tool" },
31
- { id: "x3", label: "Extracting the line items", detail: "Description, HS code, quantity, weight and value per carton." },
32
- { id: "x4", label: "classify_hs_codes", kind: "tool" },
33
- ];
34
-
35
- interface RecordData {
36
- id: string;
37
- title: string;
38
- confidence: ConfidenceLevel;
39
- fields: RecordField[];
40
- }
41
- const RECORDS: RecordData[] = [
42
- { id: "r1", title: "Carton 1 — Frozen shrimp", confidence: "high", fields: [
43
- { label: "HS code", value: "0306.17.10" },
44
- { label: "Quantity", value: "120", unit: "ctn" },
45
- { label: "Net weight", value: "1,200", unit: "kg" },
46
- { label: "Value (FOB)", value: "86,200,000", unit: "₫" },
47
- ] },
48
- { id: "r2", title: "Carton 2 — Cotton T-shirts", confidence: "medium", fields: [
49
- { label: "HS code", value: "6109.10.00", uncertain: true },
50
- { label: "Quantity", value: "500", unit: "pcs" },
51
- { label: "Net weight", value: "340", unit: "kg" },
52
- { label: "Value (FOB)", value: "41,300,000", unit: "₫" },
53
- ] },
54
- { id: "r3", title: "Carton 3 — Li-ion batteries", confidence: "low", fields: [
55
- { label: "HS code", value: "8507.60.10", uncertain: true },
56
- { label: "Quantity", value: "48", unit: "pcs" },
57
- { label: "Net weight", value: "96", unit: "kg" },
58
- { label: "Value (FOB)", value: "18,700,000", unit: "₫" },
59
- ] },
60
- ];
61
-
62
- const initialValues = () => Object.fromEntries(RECORDS.map((r) => [r.id, r.fields.map((f) => f.value)]));
63
-
64
- export function TplExtract() {
65
- const [phase, setPhase] = useState<Phase>("intake");
66
- const [statuses, setStatuses] = useState<Record<string, Status>>({});
67
- const [values, setValues] = useState<Record<string, string[]>>(initialValues);
68
-
69
- const [revealed, setRevealed] = useState(0);
70
- const [streaming, setStreaming] = useState(false);
71
- useEffect(() => {
72
- if (!streaming) return;
73
- if (revealed >= EXTRACT.length) {
74
- const t = setTimeout(() => {
75
- setStreaming(false);
76
- setPhase("review");
77
- }, 650);
78
- return () => clearTimeout(t);
79
- }
80
- const t = setTimeout(() => setRevealed((r) => r + 1), revealed === 0 ? 60 : 820);
81
- return () => clearTimeout(t);
82
- }, [streaming, revealed]);
83
-
84
- const items: AgentRunItem[] = EXTRACT.slice(0, revealed).map((s, i) => ({
85
- type: "step",
86
- ...s,
87
- status: streaming && revealed < EXTRACT.length && i === revealed - 1 ? "running" : "done",
88
- }));
89
-
90
- const start = () => {
91
- setRevealed(0);
92
- setStreaming(true);
93
- setPhase("extracting");
94
- };
95
- const reset = () => {
96
- setStreaming(false);
97
- setRevealed(0);
98
- setStatuses({});
99
- setValues(initialValues());
100
- setPhase("intake");
101
- };
102
-
103
- const setStatus = (id: string, status: Status | undefined) =>
104
- setStatuses((s) => {
105
- const n = { ...s };
106
- if (status) n[id] = status;
107
- else delete n[id];
108
- return n;
109
- });
110
- const editField = (id: string, index: number, value: string) =>
111
- setValues((v) => ({ ...v, [id]: v[id].map((x, i) => (i === index ? value : x)) }));
112
-
113
- const confirmedCount = RECORDS.filter((r) => statuses[r.id] === "confirmed").length;
114
-
115
- return (
116
- <ScrollView style={{ backgroundColor: colors.white }} contentContainerStyle={{ padding: 28, paddingBottom: 120 }}>
117
- <View style={{ maxWidth: 720, width: "100%", alignSelf: "center", gap: 16 }}>
118
- <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
119
- <View style={{ flex: 1, gap: 2 }}>
120
- <Text size="xl" weight="semibold">Capture packing list</Text>
121
- <Text size="sm" color="muted">Drop a photo; the agent extracts each line, you review and edit, then save them all to the table.</Text>
122
- </View>
123
- {phase !== "intake" ? <Button title="Start over" color="muted" onPress={reset} /> : null}
124
- </View>
125
-
126
- {phase === "intake" ? (
127
- <Card style={{ padding: 0 }}>
128
- <CardHeader><CardHeaderTitle info="Each extracted line is reviewable and editable before anything is saved.">Source photo</CardHeaderTitle></CardHeader>
129
- <CardBody>
130
- <FileDropzone onFiles={start} accept="image/*,application/pdf" height={200} label="Drop a packing list" hint="or click to choose · JPG, PNG, PDF" dropLabel="Drop to extract" />
131
- </CardBody>
132
- </Card>
133
- ) : null}
134
-
135
- {phase === "extracting" ? (
136
- <Card>
137
- <AgentRun items={items} state={streaming ? "streaming" : "done"} />
138
- </Card>
139
- ) : null}
140
-
141
- {phase === "review" ? (
142
- <>
143
- <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
144
- <Text size="sm" weight="semibold" style={{ flex: 1 }}>Extracted lines</Text>
145
- <Text size="xs" color="muted" tabular>{confirmedCount} of {RECORDS.length} confirmed</Text>
146
- </View>
147
- <Sources label="Extracted from" onOpen={() => {}} sources={[{ id: "doc", label: "packing-list.jpg", kind: "document", detail: "1 photo" }]} />
148
- <View style={{ gap: 10 }}>
149
- {RECORDS.map((r) => (
150
- <ReviewCard
151
- key={r.id}
152
- title={r.title}
153
- confidence={r.confidence}
154
- status={statuses[r.id] === "confirmed" ? "accepted" : statuses[r.id] === "removed" ? "dismissed" : "open"}
155
- acceptedLabel="Confirmed"
156
- dismissedLabel="Removed"
157
- actions={[
158
- { label: "Remove", onPress: () => setStatus(r.id, "removed"), kind: "muted" },
159
- { label: "Confirm", onPress: () => setStatus(r.id, "confirmed"), kind: "secondary" },
160
- ]}
161
- onUndo={() => setStatus(r.id, undefined)}
162
- >
163
- <RecordFields
164
- fields={r.fields.map((f, i) => ({ ...f, value: values[r.id][i] }))}
165
- onEditField={(i, value) => editField(r.id, i, value)}
166
- />
167
- </ReviewCard>
168
- ))}
169
- </View>
170
- <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
171
- <Button title="Confirm all" color="muted" onPress={() => setStatuses(Object.fromEntries(RECORDS.map((r) => [r.id, "confirmed" as Status])))} />
172
- <View style={{ flex: 1 }} />
173
- <Button
174
- title={`Save ${confirmedCount} ${confirmedCount === 1 ? "line" : "lines"}`}
175
- color="primary"
176
- disabled={confirmedCount === 0}
177
- onPress={() => setPhase("done")}
178
- />
179
- </View>
180
- </>
181
- ) : null}
182
-
183
- {phase === "done" ? (
184
- <Card>
185
- <CompletionState title={`${confirmedCount} lines saved`} summary="The confirmed lines were written to the shipment table.">
186
- <Button title="Capture another" color="secondary" onPress={reset} />
187
- </CompletionState>
188
- </Card>
189
- ) : null}
190
- </View>
191
- </ScrollView>
192
- );
193
- }
@@ -1,134 +0,0 @@
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 { Button } from "@lotics/ui/button";
6
- import { KPIStrip } from "@lotics/ui/kpi_strip";
7
- import { SegmentedControl } from "@lotics/ui/segmented_control";
8
- import { EmptyState } from "@lotics/ui/empty_state";
9
- import { ReviewCard, type ReviewAction } from "@lotics/ui/review_card";
10
- import { MatchSides, MatchSideText } from "@lotics/ui/match_sides";
11
- import type { ConfidenceLevel } from "@lotics/ui/confidence";
12
-
13
- // ─────────────────────────────────────────────────────────────────────────────
14
- // Template · Match & reconcile — the agent proposes PAIRINGS and shows its
15
- // reasoning; the human confirms, reassigns, or rejects. Each row is two-sided
16
- // (a known item ↔ the agent's proposed counterpart) with a confidence on the
17
- // bridge. High-confidence pairs batch-accept; the rest are worked one by one.
18
- // Same shape for bank-line ↔ invoice, invoice ↔ PO, dedup A ↔ B, entity
19
- // resolution. Strip the agent's reasoning and it's just a deterministic match register.
20
- // ─────────────────────────────────────────────────────────────────────────────
21
-
22
- interface M {
23
- id: string;
24
- amount: number;
25
- source: { title: string; detail: string };
26
- match?: { title: string; detail: string };
27
- /** An alternate candidate the agent considered — drives Reassign. */
28
- alt?: { title: string; detail: string };
29
- rationale: string;
30
- confidence: ConfidenceLevel;
31
- status: "open" | "accepted" | "dismissed";
32
- }
33
-
34
- const SEED: M[] = [
35
- { id: "m1", amount: 86_200_000, source: { title: "INCOMING TT — ATLAS COMPONENTS", detail: "09 Jun · 86,200,000 ₫" }, match: { title: "INV-2026-0312", detail: "Atlas Components · 86,200,000 ₫" }, rationale: "Exact amount and the invoice number appears in the transfer memo.", confidence: "high", status: "open" },
36
- { id: "m2", amount: 41_300_000, source: { title: "CRESTLINE TT PAYMENT", detail: "09 Jun · 41,300,000 ₫" }, match: { title: "INV-2026-0318", detail: "Crestline Furniture · 41,300,000 ₫" }, rationale: "Amount matches to the đồng and the payer name maps to the customer on file.", confidence: "high", status: "open" },
37
- { id: "m3", amount: 22_500_000, source: { title: "VITTORIA ACC — REF 0321", detail: "10 Jun · 22,500,000 ₫" }, match: { title: "INV-2026-0321", detail: "Vittoria Accessories · 22,500,000 ₫" }, rationale: "Reference 0321 cited; amount exact.", confidence: "high", status: "open" },
38
- { id: "m4", amount: 18_700_000, source: { title: "BRIGHTCELL JUNE", detail: "10 Jun · 18,700,000 ₫" }, match: { title: "INV-2026-0324", detail: "Brightcell Batteries · 18,700,000 ₫" }, alt: { title: "INV-2026-0319", detail: "Brightcell Batteries · 18,700,000 ₫" }, rationale: "Amount + customer match, but two open invoices share this total — confirm the period.", confidence: "medium", status: "open" },
39
- { id: "m5", amount: 12_600_000, source: { title: "MERIDIAN PART PAYMENT", detail: "12 Jun · 12,600,000 ₫" }, match: { title: "INV-2026-0326", detail: "Meridian Construction · 25,200,000 ₫" }, rationale: "Half the invoice total from the same payer — likely a partial payment.", confidence: "medium", status: "open" },
40
- { id: "m6", amount: 25_200_000, source: { title: "INCOMING TT REF 88412", detail: "11 Jun · 25,200,000 ₫" }, alt: { title: "INV-2026-0326", detail: "Meridian Construction · 25,200,000 ₫" }, rationale: "No name or reference on the line; the amount matches one open invoice but the agent isn't confident.", confidence: "low", status: "open" },
41
- { id: "m7", amount: 264_000, source: { title: "BANK CHARGES JUNE", detail: "11 Jun · 264,000 ₫" }, rationale: "No invoice fits; the description reads as a bank fee, not a receipt.", confidence: "low", status: "open" },
42
- { id: "m8", amount: 64_800_000, source: { title: "ATLAS COMPONENTS — INV-0301", detail: "08 Jun · 64,800,000 ₫" }, match: { title: "INV-2026-0301", detail: "Atlas Components · 64,800,000 ₫" }, rationale: "Exact reference and amount.", confidence: "high", status: "accepted" },
43
- ];
44
-
45
- export function TplMatch() {
46
- const [rows, setRows] = useState<M[]>(SEED);
47
- const [tab, setTab] = useState<"open" | "resolved">("open");
48
-
49
- const set = (id: string, status: M["status"]) => setRows((prev) => prev.map((r) => (r.id === id ? { ...r, status } : r)));
50
- const reassign = (id: string) =>
51
- setRows((prev) =>
52
- prev.map((r) =>
53
- r.id === id && r.alt ? { ...r, match: r.alt, alt: r.match, confidence: "medium", rationale: "Reassigned to the alternate candidate the agent considered." } : r,
54
- ),
55
- );
56
- const acceptAllHigh = () => setRows((prev) => prev.map((r) => (r.status === "open" && r.confidence === "high" && r.match ? { ...r, status: "accepted" } : r)));
57
-
58
- const open = rows.filter((r) => r.status === "open");
59
- const resolved = rows.filter((r) => r.status !== "open");
60
- const visible = tab === "open" ? open : resolved;
61
- const accepted = rows.filter((r) => r.status === "accepted");
62
- const matchedValue = accepted.reduce((s, r) => s + r.amount, 0);
63
- const unmatched = open.filter((r) => !r.match).length;
64
- const highOpen = open.filter((r) => r.confidence === "high" && r.match).length;
65
-
66
- return (
67
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
68
- <View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
69
- <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
70
- <View style={{ flex: 1, gap: 2 }}>
71
- <Text size="xl" weight="semibold">Payment matching</Text>
72
- <Text size="sm" color="muted">The agent pairs incoming payments to open invoices — you confirm, reassign, or reject</Text>
73
- </View>
74
- {tab === "open" && highOpen > 0 ? (
75
- <Button title={`Accept ${highOpen} high-confidence`} color="secondary" onPress={acceptAllHigh} />
76
- ) : null}
77
- </View>
78
-
79
- <KPIStrip
80
- items={[
81
- { label: "Proposed", value: open.length, format: "number", caption: "awaiting a decision" },
82
- { label: "Accepted", value: accepted.length, format: "number" },
83
- { label: "Unmatched", value: unmatched, format: "number", tone: unmatched > 0 ? "warning" : "default", info: "Lines the agent couldn't pair confidently — they need a human to find the counterpart or take an escape hatch." },
84
- { label: "Matched value", value: matchedValue, format: "currency", compact: true, info: "Money tied to an invoice through an accepted pairing." },
85
- ]}
86
- />
87
-
88
- <SegmentedControl
89
- accessibilityLabel="Queue"
90
- options={[
91
- { label: `Proposed · ${open.length}`, value: "open" },
92
- { label: `Resolved · ${resolved.length}`, value: "resolved" },
93
- ]}
94
- value={tab}
95
- onValueChange={setTab}
96
- />
97
-
98
- {visible.length === 0 ? (
99
- <EmptyState
100
- icon="circle-check"
101
- message={tab === "open" ? "Queue cleared" : "Nothing resolved yet"}
102
- hint={tab === "open" ? "Every proposal has been accepted or rejected" : undefined}
103
- />
104
- ) : (
105
- <View style={{ gap: 10 }}>
106
- {visible.map((r) => {
107
- const actions: ReviewAction[] = [
108
- { label: "Not a match", onPress: () => set(r.id, "dismissed"), kind: "muted" },
109
- ...(r.alt ? [{ label: r.match ? "Reassign" : "Find match", onPress: () => reassign(r.id), kind: "secondary" as const }] : []),
110
- ...(r.match ? [{ label: "Accept", onPress: () => set(r.id, "accepted"), kind: "primary" as const }] : []),
111
- ];
112
- return (
113
- <ReviewCard
114
- key={r.id}
115
- summary={r.source.title}
116
- confidence={r.confidence}
117
- rationale={r.rationale}
118
- status={r.status}
119
- acceptedLabel={r.match ? `Matched · ${r.match.title}` : "Matched"}
120
- actions={actions}
121
- >
122
- <MatchSides
123
- source={<MatchSideText title={r.source.title} detail={r.source.detail} />}
124
- match={r.match ? <MatchSideText title={r.match.title} detail={r.match.detail} align="right" /> : undefined}
125
- />
126
- </ReviewCard>
127
- );
128
- })}
129
- </View>
130
- )}
131
- </View>
132
- </ScrollView>
133
- );
134
- }