@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.
- package/AGENTS.md +128 -92
- package/examples/tpl_dieline.tsx +3 -3
- package/examples/tpl_documents.tsx +790 -0
- package/examples/tpl_lookup.tsx +37 -10
- package/examples/tpl_task_board.tsx +32 -19
- package/examples/tpl_tasks.tsx +32 -19
- package/package.json +2 -9
- package/src/agent_run.tsx +1 -1
- package/src/change_review.tsx +732 -236
- package/src/composer.tsx +22 -1
- package/src/confidence.tsx +1 -1
- package/src/control_surface.ts +0 -14
- package/src/finding.tsx +112 -80
- package/src/floating_action_bar.tsx +1 -1
- package/src/locale.tsx +22 -16
- package/src/sources.tsx +8 -5
- package/examples/tpl_assistant.tsx +0 -174
- package/examples/tpl_briefing.tsx +0 -121
- package/examples/tpl_compare.tsx +0 -133
- package/examples/tpl_crosscheck.tsx +0 -120
- package/examples/tpl_draft.tsx +0 -163
- package/examples/tpl_extract.tsx +0 -193
- package/examples/tpl_match.tsx +0 -134
- package/examples/tpl_ratedesk.tsx +0 -386
- package/examples/tpl_triage.tsx +0 -112
- package/src/discrepancy.tsx +0 -114
- package/src/match_sides.tsx +0 -79
- package/src/record_fields.tsx +0 -68
- package/src/review_card.tsx +0 -172
- package/src/scored_option.tsx +0 -138
- package/src/spec_list.tsx +0 -81
- package/src/triage_row.tsx +0 -99
package/src/match_sides.tsx
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
2
|
-
import { StyleSheet, View } from "react-native";
|
|
3
|
-
import { colors } from "./colors";
|
|
4
|
-
import { Text } from "./text";
|
|
5
|
-
import { Icon } from "./icon";
|
|
6
|
-
|
|
7
|
-
export interface MatchSideTextProps {
|
|
8
|
-
/** Primary line — the record name / id. */
|
|
9
|
-
title: string;
|
|
10
|
-
/** Secondary line — amount, date, customer. */
|
|
11
|
-
detail?: string;
|
|
12
|
-
/** Right-align for the proposed (right) side of a pairing. */
|
|
13
|
-
align?: "left" | "right";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The DEFAULT side content — a title + a muted detail line. Pass it into a
|
|
18
|
-
* `MatchSides` slot for the common case; drop a different node in when a side
|
|
19
|
-
* needs more (a `Peek` to preview the record, an interactive card, a thumbnail).
|
|
20
|
-
*/
|
|
21
|
-
export function MatchSideText({ title, detail, align }: MatchSideTextProps) {
|
|
22
|
-
const right = align === "right";
|
|
23
|
-
return (
|
|
24
|
-
<>
|
|
25
|
-
<Text size="sm" weight="semibold" numberOfLines={1} align={right ? "right" : undefined}>
|
|
26
|
-
{title}
|
|
27
|
-
</Text>
|
|
28
|
-
{detail ? (
|
|
29
|
-
<Text size="xs" color="muted" numberOfLines={1} align={right ? "right" : undefined}>
|
|
30
|
-
{detail}
|
|
31
|
-
</Text>
|
|
32
|
-
) : null}
|
|
33
|
-
</>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface MatchSidesProps {
|
|
38
|
-
/** Left content — ANY node. `MatchSideText` for the default, or a `Peek` /
|
|
39
|
-
* interactive card / thumbnail when a side needs to do more. */
|
|
40
|
-
source: ReactNode;
|
|
41
|
-
/** Right content. Omit → the "no confident match" placeholder. */
|
|
42
|
-
match?: ReactNode;
|
|
43
|
-
/** The connector between the sides. Default: a right arrow. Override to carry
|
|
44
|
-
* a swap control, a label, or a different glyph. */
|
|
45
|
-
connector?: ReactNode;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* The two-sided PAIRING — a left slot, a connector, a right slot. A pure LAYOUT
|
|
50
|
-
* primitive: it owns the two-column arrangement and the connector, the CONTENT
|
|
51
|
-
* of each side is yours (`MatchSideText`, a `Peek`, anything). The hero of a
|
|
52
|
-
* reconciliation; the confidence, reason, and decision come from the `ReviewCard`.
|
|
53
|
-
*/
|
|
54
|
-
export function MatchSides(props: MatchSidesProps) {
|
|
55
|
-
return (
|
|
56
|
-
<View style={styles.pair}>
|
|
57
|
-
<View style={styles.side}>{props.source}</View>
|
|
58
|
-
{props.connector ?? <Icon name="arrow-right" size={16} color={colors.zinc[400]} />}
|
|
59
|
-
<View style={[styles.side, styles.sideRight]}>
|
|
60
|
-
{props.match ?? (
|
|
61
|
-
<>
|
|
62
|
-
<Text size="sm" weight="medium" color="muted" align="right" numberOfLines={1}>
|
|
63
|
-
No confident match
|
|
64
|
-
</Text>
|
|
65
|
-
<Text size="xs" color="muted" align="right" numberOfLines={1}>
|
|
66
|
-
Pick a counterpart
|
|
67
|
-
</Text>
|
|
68
|
-
</>
|
|
69
|
-
)}
|
|
70
|
-
</View>
|
|
71
|
-
</View>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const styles = StyleSheet.create({
|
|
76
|
-
pair: { flexDirection: "row", alignItems: "center", gap: 14 },
|
|
77
|
-
side: { flex: 1, gap: 3, minWidth: 0 },
|
|
78
|
-
sideRight: { alignItems: "flex-end" },
|
|
79
|
-
});
|
package/src/record_fields.tsx
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { StyleSheet, View } from "react-native";
|
|
2
|
-
import { solid } from "./colors";
|
|
3
|
-
import { Text } from "./text";
|
|
4
|
-
import { InlineTextInput } from "./inline_text_input";
|
|
5
|
-
|
|
6
|
-
export interface RecordField {
|
|
7
|
-
label: string;
|
|
8
|
-
value: string;
|
|
9
|
-
/** Unit suffix shown after the value (₫, kg, pcs). */
|
|
10
|
-
unit?: string;
|
|
11
|
-
/** Mark a low-confidence field worth double-checking — a subtle amber dot. */
|
|
12
|
-
uncertain?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface RecordFieldsProps {
|
|
16
|
-
fields: RecordField[];
|
|
17
|
-
/** Edit a field — fired on save of the inline editor (blur / Enter). When set,
|
|
18
|
-
* each value is click-to-edit; omit for a read-only record. */
|
|
19
|
-
onEditField?: (index: number, value: string) => void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The BODY of an extracted record — a label/value list, each value click-to-edit
|
|
24
|
-
* (`InlineTextInput`) or read-only. A pure body atom: drop it into a `ReviewCard`
|
|
25
|
-
* to review-before-save (a confidence + Confirm / Remove, stacked under a Save-all
|
|
26
|
-
* → `tpl_extract`), or use it standalone as a live-edit field card (a params
|
|
27
|
-
* panel). The review MECHANICS belong to the host / `ReviewCard`, not here.
|
|
28
|
-
*/
|
|
29
|
-
export function RecordFields(props: RecordFieldsProps) {
|
|
30
|
-
return (
|
|
31
|
-
<View style={styles.fields}>
|
|
32
|
-
{props.fields.map((f, i) => (
|
|
33
|
-
<View key={`${f.label}-${i}`} style={styles.fieldRow}>
|
|
34
|
-
<View style={styles.labelCol}>
|
|
35
|
-
{f.uncertain ? <View style={styles.uncertainDot} /> : null}
|
|
36
|
-
<Text size="sm" color="muted" numberOfLines={1}>
|
|
37
|
-
{f.label}
|
|
38
|
-
</Text>
|
|
39
|
-
</View>
|
|
40
|
-
<View style={styles.valueCol}>
|
|
41
|
-
<View style={{ flex: 1 }}>
|
|
42
|
-
{props.onEditField ? (
|
|
43
|
-
<InlineTextInput value={f.value} accessibilityLabel={`Edit ${f.label}`} onSave={(next) => props.onEditField?.(i, next)} />
|
|
44
|
-
) : (
|
|
45
|
-
<Text size="sm" weight="medium" tabular>
|
|
46
|
-
{f.value}
|
|
47
|
-
</Text>
|
|
48
|
-
)}
|
|
49
|
-
</View>
|
|
50
|
-
{f.unit ? (
|
|
51
|
-
<Text size="sm" color="muted">
|
|
52
|
-
{f.unit}
|
|
53
|
-
</Text>
|
|
54
|
-
) : null}
|
|
55
|
-
</View>
|
|
56
|
-
</View>
|
|
57
|
-
))}
|
|
58
|
-
</View>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const styles = StyleSheet.create({
|
|
63
|
-
fields: { gap: 2 },
|
|
64
|
-
fieldRow: { flexDirection: "row", alignItems: "center", gap: 16, minHeight: 34 },
|
|
65
|
-
labelCol: { flexDirection: "row", alignItems: "center", gap: 6, width: 132 },
|
|
66
|
-
uncertainDot: { width: 6, height: 6, borderRadius: 999, backgroundColor: solid("amber") },
|
|
67
|
-
valueCol: { flex: 1, flexDirection: "row", alignItems: "center", gap: 6 },
|
|
68
|
-
});
|
package/src/review_card.tsx
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
2
|
-
import { StyleSheet, View } from "react-native";
|
|
3
|
-
import { colors, solid } from "./colors";
|
|
4
|
-
import { reviewCardStyle } from "./control_surface";
|
|
5
|
-
import { Text } from "./text";
|
|
6
|
-
import { Icon } from "./icon";
|
|
7
|
-
import { Button } from "./button";
|
|
8
|
-
import { Confidence, type ConfidenceLevel } from "./confidence";
|
|
9
|
-
|
|
10
|
-
export interface ReviewAction {
|
|
11
|
-
label: string;
|
|
12
|
-
onPress: () => void;
|
|
13
|
-
/** Visual weight. Default: the LAST action reads "primary" (the affirmative),
|
|
14
|
-
* the rest "muted". Use "secondary" for a non-destructive middle action
|
|
15
|
-
* (Edit / Reassign). */
|
|
16
|
-
kind?: "primary" | "secondary" | "muted";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ReviewCardProps {
|
|
20
|
-
/** Header-first: a small muted eyebrow above the body — e.g. "Proposed". */
|
|
21
|
-
eyebrow?: string;
|
|
22
|
-
/** Header-first: a headline above the body — a record name, a proposal. */
|
|
23
|
-
title?: string;
|
|
24
|
-
confidence?: ConfidenceLevel;
|
|
25
|
-
confidenceScore?: number;
|
|
26
|
-
/** One line of WHY — the rationale the human weighs. */
|
|
27
|
-
rationale?: string;
|
|
28
|
-
/** The proposal BODY — render it however fits: a `RecordFields`, a `MatchSides`
|
|
29
|
-
* pairing, a spec stack, a chip row. Omit for a title-only proposal. */
|
|
30
|
-
children?: ReactNode;
|
|
31
|
-
/** Decision actions, left→right (the last reads as the primary affirmative):
|
|
32
|
-
* Accept / Edit / Dismiss, Confirm / Remove, Accept / Reassign / Not-a-match —
|
|
33
|
-
* whatever the proposal's verbs are. Nothing applies until the host acts. */
|
|
34
|
-
actions?: ReviewAction[];
|
|
35
|
-
/** Resolved → the card COLLAPSES to a quiet one-line row (a check or ✕ + a
|
|
36
|
-
* summary + Undo), so a stack reads as a tidy checklist of what's decided. */
|
|
37
|
-
status?: "open" | "accepted" | "dismissed";
|
|
38
|
-
/** The summary text in the resolved row (defaults to `title`; required when
|
|
39
|
-
* body-first, since the body is arbitrary). */
|
|
40
|
-
summary?: string;
|
|
41
|
-
/** Resolved outcome words. Default "Accepted" / "Dismissed"; set per surface
|
|
42
|
-
* ("Confirmed" / "Removed", `Matched · …` / "Dismissed"). */
|
|
43
|
-
acceptedLabel?: string;
|
|
44
|
-
dismissedLabel?: string;
|
|
45
|
-
onUndo?: () => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* THE single-proposal review card — an AI proposal the human accepts, edits, or
|
|
50
|
-
* dismisses, never auto-applied. It owns the chrome (the shared `reviewCardStyle`
|
|
51
|
-
* surface, `Confidence` + rationale, the decision footer, the collapse-to-a-row
|
|
52
|
-
* resolved state); the proposal BODY is `children` — a `RecordFields` (an
|
|
53
|
-
* extracted record to confirm), a `MatchSides` (a pairing to reconcile), a spec
|
|
54
|
-
* stack, anything.
|
|
55
|
-
*
|
|
56
|
-
* Layout follows the props: give it an `eyebrow`/`title` and the chrome LEADS
|
|
57
|
-
* (header-first — a value or record proposal); give it only `children` and the
|
|
58
|
-
* BODY leads, the confidence + reason sitting beneath it (body-first — a pairing,
|
|
59
|
-
* where the body is the hero). For a BATCH you decide each then commit, use the
|
|
60
|
-
* `ChangeReview` engine instead.
|
|
61
|
-
*/
|
|
62
|
-
export function ReviewCard(props: ReviewCardProps) {
|
|
63
|
-
const status = props.status ?? "open";
|
|
64
|
-
|
|
65
|
-
if (status === "accepted" || status === "dismissed") {
|
|
66
|
-
const accepted = status === "accepted";
|
|
67
|
-
return (
|
|
68
|
-
<View style={styles.resolvedRow}>
|
|
69
|
-
<Icon name={accepted ? "check" : "x"} size={15} color={accepted ? solid("emerald") : colors.zinc[400]} />
|
|
70
|
-
<Text
|
|
71
|
-
size="sm"
|
|
72
|
-
weight="medium"
|
|
73
|
-
color={accepted ? "default" : "muted"}
|
|
74
|
-
numberOfLines={1}
|
|
75
|
-
style={[{ flex: 1 }, accepted ? null : styles.strike]}
|
|
76
|
-
>
|
|
77
|
-
{props.summary ?? props.title ?? ""}
|
|
78
|
-
</Text>
|
|
79
|
-
<Text size="sm" weight="medium" color="muted" numberOfLines={1}>
|
|
80
|
-
{accepted ? props.acceptedLabel ?? "Accepted" : props.dismissedLabel ?? "Dismissed"}
|
|
81
|
-
</Text>
|
|
82
|
-
{props.onUndo ? <Button title="Undo" color="muted" onPress={props.onUndo} /> : null}
|
|
83
|
-
</View>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const headed = !!(props.eyebrow || props.title);
|
|
88
|
-
const hasConfidence = props.confidence != null || props.confidenceScore != null;
|
|
89
|
-
const confidence = hasConfidence ? <Confidence level={props.confidence} score={props.confidenceScore} /> : null;
|
|
90
|
-
const actions = props.actions ?? [];
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<View style={reviewCardStyle}>
|
|
94
|
-
{/* Header-first: eyebrow + confidence, title, rationale ABOVE the body. */}
|
|
95
|
-
{props.eyebrow ? (
|
|
96
|
-
<View style={styles.headRow}>
|
|
97
|
-
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>
|
|
98
|
-
{props.eyebrow}
|
|
99
|
-
</Text>
|
|
100
|
-
{confidence}
|
|
101
|
-
</View>
|
|
102
|
-
) : null}
|
|
103
|
-
{props.title ? (
|
|
104
|
-
<View style={styles.headRow}>
|
|
105
|
-
<Text size="sm" weight="semibold" style={{ flex: 1 }} numberOfLines={2}>
|
|
106
|
-
{props.title}
|
|
107
|
-
</Text>
|
|
108
|
-
{!props.eyebrow ? confidence : null}
|
|
109
|
-
</View>
|
|
110
|
-
) : null}
|
|
111
|
-
{headed && props.rationale ? (
|
|
112
|
-
<Text size="sm" color="muted">
|
|
113
|
-
{props.rationale}
|
|
114
|
-
</Text>
|
|
115
|
-
) : null}
|
|
116
|
-
|
|
117
|
-
{props.children ? <View>{props.children}</View> : null}
|
|
118
|
-
|
|
119
|
-
{/* Body-first: the confidence + reason sit BENEATH the body, as one unit. */}
|
|
120
|
-
{!headed && (hasConfidence || props.rationale) ? (
|
|
121
|
-
<View style={styles.basis}>
|
|
122
|
-
{confidence}
|
|
123
|
-
{props.rationale ? (
|
|
124
|
-
<Text size="sm" color="muted">
|
|
125
|
-
{props.rationale}
|
|
126
|
-
</Text>
|
|
127
|
-
) : null}
|
|
128
|
-
</View>
|
|
129
|
-
) : null}
|
|
130
|
-
|
|
131
|
-
{actions.length > 0 ? (
|
|
132
|
-
<View style={styles.footer}>
|
|
133
|
-
{actions.map((a, i) => {
|
|
134
|
-
const kind = a.kind ?? (i === actions.length - 1 ? "primary" : "muted");
|
|
135
|
-
return <Button key={a.label} title={a.label} color={kind} onPress={a.onPress} />;
|
|
136
|
-
})}
|
|
137
|
-
</View>
|
|
138
|
-
) : null}
|
|
139
|
-
</View>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const styles = StyleSheet.create({
|
|
144
|
-
headRow: { flexDirection: "row", alignItems: "center", gap: 10 },
|
|
145
|
-
// Body-first: confidence on top, the reason beneath — "this sure, because…".
|
|
146
|
-
basis: { alignItems: "flex-start", gap: 6 },
|
|
147
|
-
// The decisions sit below a hairline so the CTA row reads as a deliberate
|
|
148
|
-
// footer, not buttons crowding the body.
|
|
149
|
-
footer: {
|
|
150
|
-
flexDirection: "row",
|
|
151
|
-
alignItems: "center",
|
|
152
|
-
justifyContent: "flex-end",
|
|
153
|
-
gap: 8,
|
|
154
|
-
borderTopWidth: 1,
|
|
155
|
-
borderTopColor: colors.zinc[100],
|
|
156
|
-
paddingTop: 14,
|
|
157
|
-
},
|
|
158
|
-
resolvedRow: {
|
|
159
|
-
flexDirection: "row",
|
|
160
|
-
alignItems: "center",
|
|
161
|
-
gap: 10,
|
|
162
|
-
backgroundColor: colors.white,
|
|
163
|
-
borderWidth: 1,
|
|
164
|
-
borderColor: colors.zinc[100],
|
|
165
|
-
borderRadius: 10,
|
|
166
|
-
paddingLeft: 12,
|
|
167
|
-
paddingRight: 8,
|
|
168
|
-
paddingVertical: 6,
|
|
169
|
-
minHeight: 44,
|
|
170
|
-
},
|
|
171
|
-
strike: { textDecorationLine: "line-through" },
|
|
172
|
-
});
|
package/src/scored_option.tsx
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { StyleSheet, View, type ViewStyle } from "react-native";
|
|
2
|
-
import { colors } from "./colors";
|
|
3
|
-
import { Text } from "./text";
|
|
4
|
-
import { Button } from "./button";
|
|
5
|
-
import { SpecList, type SpecRow } from "./spec_list";
|
|
6
|
-
|
|
7
|
-
export interface ScoredOptionProps {
|
|
8
|
-
/** Rank in the shortlist (1 = top). */
|
|
9
|
-
rank?: number;
|
|
10
|
-
title: string;
|
|
11
|
-
/** Secondary line — the provider, the route summary. */
|
|
12
|
-
subtitle?: string;
|
|
13
|
-
/** 0–1 score the agent assigned — drives the score bar + figure. */
|
|
14
|
-
score?: number;
|
|
15
|
-
/** One line on why it ranks where it does. */
|
|
16
|
-
rationale?: string;
|
|
17
|
-
/** Key specs (price, transit, …) — rendered as a dense `SpecList`. */
|
|
18
|
-
specs?: SpecRow[];
|
|
19
|
-
/** The agent's top pick — a "RECOMMENDED" label + a heavier border. */
|
|
20
|
-
recommended?: boolean;
|
|
21
|
-
selected?: boolean;
|
|
22
|
-
onSelect?: () => void;
|
|
23
|
-
selectLabel?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* An AI-RANKED candidate in a shortlist — rank, score, the agent's one-line
|
|
28
|
-
* reason (a margin note), and the key specs — that the human picks from. The
|
|
29
|
-
* agent scores and orders; the choice stays the human's. Mark the agent's top
|
|
30
|
-
* pick `recommended` (a label + heavier border, no colour). The unit of an AI
|
|
31
|
-
* compare / shortlist / recommend surface (quotes, carriers, suppliers, plans).
|
|
32
|
-
* Monochrome and iconless. Stack several, highest rank first.
|
|
33
|
-
*/
|
|
34
|
-
export function ScoredOption(props: ScoredOptionProps) {
|
|
35
|
-
const { rank, title, subtitle, score, rationale, specs, recommended, selected, onSelect, selectLabel } = props;
|
|
36
|
-
const pct = score != null ? Math.round(score * 100) : null;
|
|
37
|
-
return (
|
|
38
|
-
<View style={[styles.card, recommended ? styles.recCard : null, selected ? styles.selected : null]}>
|
|
39
|
-
{recommended ? (
|
|
40
|
-
<Text size="xs" color="default" weight="semibold">
|
|
41
|
-
Recommended
|
|
42
|
-
</Text>
|
|
43
|
-
) : null}
|
|
44
|
-
|
|
45
|
-
<View style={styles.head}>
|
|
46
|
-
{rank != null ? (
|
|
47
|
-
<View style={styles.rank}>
|
|
48
|
-
<Text size="sm" weight="semibold" style={{ color: colors.zinc[700] }}>
|
|
49
|
-
{rank}
|
|
50
|
-
</Text>
|
|
51
|
-
</View>
|
|
52
|
-
) : null}
|
|
53
|
-
<View style={styles.titleCol}>
|
|
54
|
-
<Text size="md" weight="semibold" numberOfLines={1}>
|
|
55
|
-
{title}
|
|
56
|
-
</Text>
|
|
57
|
-
{subtitle ? (
|
|
58
|
-
<Text size="xs" color="muted" numberOfLines={1}>
|
|
59
|
-
{subtitle}
|
|
60
|
-
</Text>
|
|
61
|
-
) : null}
|
|
62
|
-
</View>
|
|
63
|
-
{pct != null ? (
|
|
64
|
-
<View style={styles.scoreCol}>
|
|
65
|
-
<Text size="lg" weight="semibold" tabular>
|
|
66
|
-
{pct}
|
|
67
|
-
</Text>
|
|
68
|
-
<Text size="xs" color="muted">
|
|
69
|
-
score
|
|
70
|
-
</Text>
|
|
71
|
-
</View>
|
|
72
|
-
) : null}
|
|
73
|
-
</View>
|
|
74
|
-
|
|
75
|
-
{pct != null ? (
|
|
76
|
-
<View style={styles.track}>
|
|
77
|
-
<View style={[styles.fill, { width: `${pct}%` }]} />
|
|
78
|
-
</View>
|
|
79
|
-
) : null}
|
|
80
|
-
|
|
81
|
-
{rationale ? (
|
|
82
|
-
<View style={styles.note}>
|
|
83
|
-
<Text size="sm" color="muted">
|
|
84
|
-
{rationale}
|
|
85
|
-
</Text>
|
|
86
|
-
</View>
|
|
87
|
-
) : null}
|
|
88
|
-
|
|
89
|
-
{specs && specs.length > 0 ? <SpecList rows={specs} dense /> : null}
|
|
90
|
-
|
|
91
|
-
{onSelect ? (
|
|
92
|
-
<View style={styles.footer}>
|
|
93
|
-
<Button
|
|
94
|
-
title={selected ? "Selected" : selectLabel ?? "Choose"}
|
|
95
|
-
icon={selected ? "check" : undefined}
|
|
96
|
-
color={selected ? "secondary" : "primary"}
|
|
97
|
-
onPress={onSelect}
|
|
98
|
-
/>
|
|
99
|
-
</View>
|
|
100
|
-
) : null}
|
|
101
|
-
</View>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const RING: ViewStyle = { boxShadow: `0 0 0 2px ${colors.zinc[900]}` } as ViewStyle;
|
|
106
|
-
|
|
107
|
-
const styles = StyleSheet.create({
|
|
108
|
-
card: {
|
|
109
|
-
borderWidth: 1,
|
|
110
|
-
borderColor: colors.border,
|
|
111
|
-
backgroundColor: colors.white,
|
|
112
|
-
borderRadius: 12,
|
|
113
|
-
padding: 16,
|
|
114
|
-
gap: 12,
|
|
115
|
-
},
|
|
116
|
-
recCard: { borderColor: colors.zinc[400] },
|
|
117
|
-
selected: RING,
|
|
118
|
-
footer: { flexDirection: "row", justifyContent: "flex-end" },
|
|
119
|
-
head: { flexDirection: "row", alignItems: "center", gap: 12 },
|
|
120
|
-
rank: {
|
|
121
|
-
width: 28,
|
|
122
|
-
height: 28,
|
|
123
|
-
borderRadius: 8,
|
|
124
|
-
alignItems: "center",
|
|
125
|
-
justifyContent: "center",
|
|
126
|
-
backgroundColor: colors.zinc[100],
|
|
127
|
-
},
|
|
128
|
-
titleCol: { flex: 1, gap: 1 },
|
|
129
|
-
scoreCol: { alignItems: "flex-end" },
|
|
130
|
-
track: {
|
|
131
|
-
height: 6,
|
|
132
|
-
borderRadius: 999,
|
|
133
|
-
backgroundColor: colors.zinc[100],
|
|
134
|
-
overflow: "hidden",
|
|
135
|
-
},
|
|
136
|
-
fill: { height: "100%", borderRadius: 999, backgroundColor: colors.zinc[900] },
|
|
137
|
-
note: { borderLeftWidth: 2, borderLeftColor: colors.zinc[300], paddingLeft: 10 },
|
|
138
|
-
});
|
package/src/spec_list.tsx
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { StyleSheet, View } from "react-native";
|
|
2
|
-
import { Text } from "./text";
|
|
3
|
-
import { Divider } from "./divider";
|
|
4
|
-
|
|
5
|
-
export interface SpecRow {
|
|
6
|
-
/** The line label (left). */
|
|
7
|
-
label: string;
|
|
8
|
-
/** The value (right). A number renders grouped + tabular. */
|
|
9
|
-
value: string | number;
|
|
10
|
-
/** Unit after the value (%, mm, kg, ₫). */
|
|
11
|
-
unit?: string;
|
|
12
|
-
/** A heavier value — a headline figure or a sub-total inside the list. */
|
|
13
|
-
emphasis?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface SpecListProps {
|
|
17
|
-
rows: SpecRow[];
|
|
18
|
-
/** A pinned summary row, set off by a divider above and a heavier weight —
|
|
19
|
-
* the total of a duty / cost / dimension breakdown. */
|
|
20
|
-
total?: SpecRow;
|
|
21
|
-
/** Tighter rows + xs labels for a dense side panel. */
|
|
22
|
-
dense?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* A labeled-value breakdown — the structured "spec sheet": a duty table, a
|
|
27
|
-
* dimensions list, a cost split, the exact numbers behind an answer. Each row is
|
|
28
|
-
* just label → value (· unit); an optional `total` pins the sum under a divider.
|
|
29
|
-
* Deliberately minimal — two columns, hierarchy from weight and tabular figures.
|
|
30
|
-
* The right-hand panel of a lookup / answer surface, the specs of a compared
|
|
31
|
-
* option.
|
|
32
|
-
*/
|
|
33
|
-
export function SpecList({ rows, total, dense }: SpecListProps) {
|
|
34
|
-
return (
|
|
35
|
-
<View>
|
|
36
|
-
{rows.map((r, i) => (
|
|
37
|
-
<SpecLine key={`${r.label}-${i}`} row={r} dense={dense} />
|
|
38
|
-
))}
|
|
39
|
-
{total ? (
|
|
40
|
-
<>
|
|
41
|
-
<Divider paddingVertical={dense ? 5 : 7} />
|
|
42
|
-
<SpecLine row={{ ...total, emphasis: true }} dense={dense} isTotal />
|
|
43
|
-
</>
|
|
44
|
-
) : null}
|
|
45
|
-
</View>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function SpecLine({ row, dense, isTotal }: { row: SpecRow; dense?: boolean; isTotal?: boolean }) {
|
|
50
|
-
const { label, value, unit, emphasis } = row;
|
|
51
|
-
const display = typeof value === "number" ? value.toLocaleString("en-US") : value;
|
|
52
|
-
return (
|
|
53
|
-
<View style={[styles.row, { minHeight: dense ? 26 : 32 }]}>
|
|
54
|
-
<Text
|
|
55
|
-
size={dense ? "xs" : "sm"}
|
|
56
|
-
color={isTotal ? "default" : "muted"}
|
|
57
|
-
weight={isTotal ? "semibold" : "regular"}
|
|
58
|
-
numberOfLines={1}
|
|
59
|
-
style={styles.labelText}
|
|
60
|
-
>
|
|
61
|
-
{label}
|
|
62
|
-
</Text>
|
|
63
|
-
<View style={styles.valueRow}>
|
|
64
|
-
<Text size={dense && !isTotal ? "sm" : "md"} weight={emphasis ? "semibold" : "medium"} tabular>
|
|
65
|
-
{display}
|
|
66
|
-
</Text>
|
|
67
|
-
{unit ? (
|
|
68
|
-
<Text size={dense ? "xs" : "sm"} color="muted">
|
|
69
|
-
{unit}
|
|
70
|
-
</Text>
|
|
71
|
-
) : null}
|
|
72
|
-
</View>
|
|
73
|
-
</View>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const styles = StyleSheet.create({
|
|
78
|
-
row: { flexDirection: "row", alignItems: "baseline", justifyContent: "space-between", gap: 16 },
|
|
79
|
-
labelText: { flexShrink: 1 },
|
|
80
|
-
valueRow: { flexDirection: "row", alignItems: "baseline", gap: 4 },
|
|
81
|
-
});
|
package/src/triage_row.tsx
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { StyleSheet, View } from "react-native";
|
|
2
|
-
import { colors } from "./colors";
|
|
3
|
-
import { Text } from "./text";
|
|
4
|
-
import { Button } from "./button";
|
|
5
|
-
import { Badge } from "./badge";
|
|
6
|
-
import { Confidence, type ConfidenceLevel } from "./confidence";
|
|
7
|
-
|
|
8
|
-
export interface TriageRowProps {
|
|
9
|
-
/** The incoming item's headline — a subject, a lead name, a ticket. */
|
|
10
|
-
title: string;
|
|
11
|
-
/** A one-line preview / snippet of the item. */
|
|
12
|
-
preview?: string;
|
|
13
|
-
/** Right meta — time, sender, channel. */
|
|
14
|
-
meta?: string;
|
|
15
|
-
/** The AI's classification, rendered as a neutral badge on the call line. */
|
|
16
|
-
category?: { label: string };
|
|
17
|
-
/** The AI's suggested action — the specific task to take ("Assign to Nguyen",
|
|
18
|
-
* "Apply the payment"). Pairs with `category`, doesn't repeat it. */
|
|
19
|
-
suggestedAction?: string;
|
|
20
|
-
confidence?: ConfidenceLevel;
|
|
21
|
-
confidenceScore?: number;
|
|
22
|
-
/** Accept the AI's call — its classification + action. */
|
|
23
|
-
onAccept?: () => void;
|
|
24
|
-
/** Override — the host opens a reclassify control. */
|
|
25
|
-
onOverride?: () => void;
|
|
26
|
-
onDismiss?: () => void;
|
|
27
|
-
/** Resolved → settles to a quiet outcome line. */
|
|
28
|
-
status?: "open" | "accepted" | "dismissed";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* An incoming item the agent has TRIAGED — the item up top (title · preview ·
|
|
33
|
-
* time), then the agent's CALL (a classification badge + the suggested action +
|
|
34
|
-
* confidence), then the decision (accept / reclassify / dismiss). High-confidence
|
|
35
|
-
* items batch-accept from the host. The unit of an AI inbox / intake / routing
|
|
36
|
-
* queue. The classification is the agent's; the decision stays the human's.
|
|
37
|
-
*/
|
|
38
|
-
export function TriageRow(props: TriageRowProps) {
|
|
39
|
-
const status = props.status ?? "open";
|
|
40
|
-
const resolved = status !== "open";
|
|
41
|
-
const hasConfidence = props.confidence != null || props.confidenceScore != null;
|
|
42
|
-
return (
|
|
43
|
-
<View style={[styles.card, resolved ? styles.resolved : null]}>
|
|
44
|
-
<View style={styles.titleRow}>
|
|
45
|
-
<Text size="sm" weight="semibold" style={{ flex: 1 }} numberOfLines={1}>
|
|
46
|
-
{props.title}
|
|
47
|
-
</Text>
|
|
48
|
-
{props.meta ? (
|
|
49
|
-
<Text size="xs" color="muted" numberOfLines={1}>
|
|
50
|
-
{props.meta}
|
|
51
|
-
</Text>
|
|
52
|
-
) : null}
|
|
53
|
-
</View>
|
|
54
|
-
|
|
55
|
-
{props.preview ? (
|
|
56
|
-
<Text size="sm" color="muted" numberOfLines={1}>
|
|
57
|
-
{props.preview}
|
|
58
|
-
</Text>
|
|
59
|
-
) : null}
|
|
60
|
-
|
|
61
|
-
<View style={styles.callRow}>
|
|
62
|
-
{props.category ? <Badge label={props.category.label} /> : null}
|
|
63
|
-
{props.suggestedAction ? (
|
|
64
|
-
<Text size="sm" weight="medium" numberOfLines={1} style={{ flexShrink: 1 }}>
|
|
65
|
-
{props.suggestedAction}
|
|
66
|
-
</Text>
|
|
67
|
-
) : null}
|
|
68
|
-
{hasConfidence && !resolved ? <Confidence level={props.confidence} score={props.confidenceScore} /> : null}
|
|
69
|
-
</View>
|
|
70
|
-
|
|
71
|
-
{resolved ? (
|
|
72
|
-
<Text size="xs" color="muted" weight="medium">
|
|
73
|
-
{status === "accepted" ? "Accepted" : "Dismissed"}
|
|
74
|
-
</Text>
|
|
75
|
-
) : (
|
|
76
|
-
<View style={styles.footer}>
|
|
77
|
-
{props.onOverride ? <Button title="Reclassify" color="muted" onPress={props.onOverride} /> : null}
|
|
78
|
-
{props.onDismiss ? <Button title="Dismiss" color="muted" onPress={props.onDismiss} /> : null}
|
|
79
|
-
{props.onAccept ? <Button title="Accept" color="primary" onPress={props.onAccept} /> : null}
|
|
80
|
-
</View>
|
|
81
|
-
)}
|
|
82
|
-
</View>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const styles = StyleSheet.create({
|
|
87
|
-
card: {
|
|
88
|
-
borderWidth: 1,
|
|
89
|
-
borderColor: colors.border,
|
|
90
|
-
backgroundColor: colors.white,
|
|
91
|
-
borderRadius: 12,
|
|
92
|
-
padding: 16,
|
|
93
|
-
gap: 10,
|
|
94
|
-
},
|
|
95
|
-
resolved: { backgroundColor: colors.zinc[50] },
|
|
96
|
-
titleRow: { flexDirection: "row", alignItems: "center", gap: 10 },
|
|
97
|
-
callRow: { flexDirection: "row", alignItems: "center", gap: 10 },
|
|
98
|
-
footer: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 8 },
|
|
99
|
-
});
|