@lotics/ui 7.19.3 → 9.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 +298 -159
- package/examples/tpl_allocate.tsx +2 -2
- package/examples/tpl_attendance.tsx +2 -2
- package/examples/tpl_calendar.tsx +1 -1
- package/examples/tpl_dashboard.tsx +1 -1
- package/examples/tpl_dieline.tsx +3 -3
- package/examples/tpl_documents.tsx +790 -0
- package/examples/tpl_item_list.tsx +1015 -124
- package/examples/tpl_lookup.tsx +37 -10
- package/examples/tpl_pick.tsx +3 -3
- package/examples/tpl_pivot.tsx +1 -1
- package/examples/tpl_record.tsx +1354 -0
- package/examples/tpl_report.tsx +7 -7
- package/examples/tpl_rollup.tsx +6 -6
- package/examples/tpl_shifts.tsx +2 -2
- package/examples/tpl_statements.tsx +221 -0
- package/examples/tpl_stock.tsx +7 -7
- package/examples/tpl_task_board.tsx +48 -32
- package/examples/tpl_tasks.tsx +47 -47
- package/examples/tpl_tower.tsx +2 -2
- package/package.json +9 -9
- package/src/agent_run.tsx +1 -1
- package/src/capture_row.tsx +59 -0
- package/src/change_review.tsx +732 -236
- package/src/checklist.tsx +104 -0
- package/src/chip.tsx +12 -3
- package/src/composer.tsx +22 -1
- package/src/confidence.tsx +1 -1
- package/src/control_surface.ts +0 -14
- package/src/detail_row.tsx +137 -10
- package/src/finding.tsx +112 -80
- package/src/floating_action_bar.tsx +1 -1
- package/src/inline_date_picker.tsx +8 -3
- package/src/inline_edit.tsx +40 -10
- package/src/inline_member_select.tsx +3 -0
- package/src/inline_number_input.tsx +5 -2
- package/src/inline_select.tsx +8 -3
- package/src/inline_tag_select.tsx +140 -0
- package/src/inline_text_input.tsx +5 -2
- package/src/inline_time_picker.tsx +5 -2
- package/src/ledger.tsx +220 -0
- package/src/locale.tsx +43 -16
- package/src/progress_bar.tsx +32 -1
- package/src/record_summary.tsx +101 -0
- package/src/section_heading.tsx +16 -8
- package/src/sources.tsx +8 -5
- package/src/suggestion_chip.tsx +47 -0
- package/src/use_section_nav.test.ts +69 -0
- package/src/use_section_nav.ts +59 -0
- package/examples/tpl_assistant.tsx +0 -174
- package/examples/tpl_billing.tsx +0 -344
- package/examples/tpl_briefing.tsx +0 -121
- package/examples/tpl_compare.tsx +0 -133
- package/examples/tpl_crosscheck.tsx +0 -120
- package/examples/tpl_detail.tsx +0 -232
- package/examples/tpl_directory.tsx +0 -260
- package/examples/tpl_draft.tsx +0 -163
- package/examples/tpl_extract.tsx +0 -193
- package/examples/tpl_intake.tsx +0 -206
- package/examples/tpl_match.tsx +0 -134
- package/examples/tpl_order.tsx +0 -482
- package/examples/tpl_quick.tsx +0 -211
- package/examples/tpl_ratedesk.tsx +0 -386
- package/examples/tpl_record_plain.tsx +0 -259
- package/examples/tpl_settings.tsx +0 -178
- package/examples/tpl_timeline.tsx +0 -244
- package/examples/tpl_triage.tsx +0 -112
- package/examples/tpl_wizard.tsx +0 -223
- 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/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
|
-
});
|