@lotics/ui 4.8.0 → 5.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 +77 -36
- package/examples/tpl_assistant.tsx +25 -4
- package/examples/tpl_dieline.tsx +2 -2
- package/examples/tpl_extract.tsx +18 -10
- package/examples/tpl_lookup.tsx +80 -67
- package/examples/tpl_match.tsx +25 -14
- package/examples/tpl_ratedesk.tsx +375 -0
- package/examples/tpl_report.tsx +413 -0
- package/package.json +4 -4
- package/src/agent_run.tsx +1 -1
- package/src/change_review.tsx +103 -96
- package/src/combobox.tsx +22 -4
- package/src/confidence.tsx +1 -1
- package/src/control_surface.ts +27 -1
- package/src/icon.tsx +2 -0
- package/src/match_sides.tsx +79 -0
- package/src/record_fields.tsx +68 -0
- package/src/review_card.tsx +172 -0
- package/src/search_input.tsx +14 -12
- package/src/match_row.tsx +0 -133
- package/src/record_review.tsx +0 -149
- package/src/suggestion.tsx +0 -92
|
@@ -0,0 +1,172 @@
|
|
|
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" shape="rounded" 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} shape="rounded" 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/search_input.tsx
CHANGED
|
@@ -49,25 +49,27 @@ export function SearchInput(props: SearchInputProps) {
|
|
|
49
49
|
onKeyPress={handleKeyPress}
|
|
50
50
|
icon="search"
|
|
51
51
|
clearable
|
|
52
|
-
style={styles.
|
|
52
|
+
style={styles.search}
|
|
53
53
|
/>
|
|
54
54
|
</View>
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const styles = StyleSheet.create({
|
|
59
|
-
// Search reads as a view-control by
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
// Search reads as a view-control by its leading search glyph + white fill —
|
|
60
|
+
// NOT by a distinct shape. It shares the rounded-8 radius of every other
|
|
61
|
+
// input/picker (TextInputField, Combobox, the date filter), so a toolbar of
|
|
62
|
+
// controls reads as one consistent band. The white fill still pops against
|
|
63
|
+
// the zinc-50 canvas where most toolbars live; a zinc-300 border (one step up
|
|
64
|
+
// from the form-input hairline) keeps it defined on white cards too, where
|
|
65
|
+
// the fill alone would vanish. 40px (TextInputField default) — the system
|
|
66
|
+
// control height every band aligns to.
|
|
67
|
+
search: {
|
|
68
|
+
borderRadius: 8,
|
|
67
69
|
backgroundColor: colors.white,
|
|
68
|
-
// A thin 1px resting border (TextInputField's default width) —
|
|
69
|
-
//
|
|
70
|
-
//
|
|
70
|
+
// A thin 1px resting border (TextInputField's default width) — interaction
|
|
71
|
+
// emphasis comes from the input's own focus outline, so it stays quiet at
|
|
72
|
+
// rest. zinc-300 keeps it defined on white cards.
|
|
71
73
|
borderColor: colors.zinc[300],
|
|
72
74
|
},
|
|
73
75
|
});
|
package/src/match_row.tsx
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { StyleSheet, View } from "react-native";
|
|
2
|
-
import { colors } from "./colors";
|
|
3
|
-
import { Text } from "./text";
|
|
4
|
-
import { Icon } from "./icon";
|
|
5
|
-
import { Button } from "./button";
|
|
6
|
-
import { Confidence, type ConfidenceLevel } from "./confidence";
|
|
7
|
-
|
|
8
|
-
export interface MatchSide {
|
|
9
|
-
/** Primary line — the record name / id. */
|
|
10
|
-
title: string;
|
|
11
|
-
/** Secondary line — amount, date, customer. */
|
|
12
|
-
detail?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface MatchRowProps {
|
|
16
|
-
/** The known item we're finding a counterpart for. */
|
|
17
|
-
source: MatchSide;
|
|
18
|
-
/** The agent's proposed counterpart. Omit → the "no confident match" state. */
|
|
19
|
-
match?: MatchSide;
|
|
20
|
-
/** One line of WHY the agent paired them. */
|
|
21
|
-
rationale?: string;
|
|
22
|
-
confidence?: ConfidenceLevel;
|
|
23
|
-
confidenceScore?: number;
|
|
24
|
-
onAccept?: () => void;
|
|
25
|
-
/** Pick a different counterpart — the host opens its candidate list. */
|
|
26
|
-
onReassign?: () => void;
|
|
27
|
-
onDismiss?: () => void;
|
|
28
|
-
acceptLabel?: string;
|
|
29
|
-
/** Resolved → settles to a quiet outcome line. */
|
|
30
|
-
status?: "open" | "accepted" | "dismissed";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* An AI-proposed PAIRING — a known item, an arrow, the agent's proposed
|
|
35
|
-
* counterpart; a confidence meter and one muted line of why. The human accepts,
|
|
36
|
-
* reassigns, or rejects; nothing links on its own. Deliberately spare: two
|
|
37
|
-
* sides, an arrow, the reason. The unit of an AI reconciliation / dedup /
|
|
38
|
-
* correlation queue. Unlike `Suggestion` (a single proposed value) this is
|
|
39
|
-
* two-sided; unlike the deterministic reconcile recipe, the agent reasons it.
|
|
40
|
-
*/
|
|
41
|
-
export function MatchRow(props: MatchRowProps) {
|
|
42
|
-
const status = props.status ?? "open";
|
|
43
|
-
const resolved = status !== "open";
|
|
44
|
-
const hasMatch = props.match != null;
|
|
45
|
-
const hasConfidence = props.confidence != null || props.confidenceScore != null;
|
|
46
|
-
|
|
47
|
-
if (resolved) {
|
|
48
|
-
return (
|
|
49
|
-
<View style={[styles.card, styles.resolved]}>
|
|
50
|
-
<View style={styles.pair}>
|
|
51
|
-
<Side side={props.source} />
|
|
52
|
-
</View>
|
|
53
|
-
<Text size="xs" color="muted" weight="medium">
|
|
54
|
-
{status === "accepted" ? (hasMatch ? `Matched · ${props.match!.title}` : "Matched") : "Dismissed"}
|
|
55
|
-
</Text>
|
|
56
|
-
</View>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<View style={styles.card}>
|
|
62
|
-
<View style={styles.pair}>
|
|
63
|
-
<Side side={props.source} />
|
|
64
|
-
<Icon name="arrow-right" size={16} color={colors.zinc[400]} />
|
|
65
|
-
{hasMatch ? (
|
|
66
|
-
<Side side={props.match!} right />
|
|
67
|
-
) : (
|
|
68
|
-
<View style={[styles.side, styles.sideRight]}>
|
|
69
|
-
<Text size="sm" weight="medium" color="muted" align="right" numberOfLines={1}>
|
|
70
|
-
No confident match
|
|
71
|
-
</Text>
|
|
72
|
-
<Text size="xs" color="muted" align="right" numberOfLines={1}>
|
|
73
|
-
Pick a counterpart
|
|
74
|
-
</Text>
|
|
75
|
-
</View>
|
|
76
|
-
)}
|
|
77
|
-
</View>
|
|
78
|
-
|
|
79
|
-
{hasConfidence || props.rationale ? (
|
|
80
|
-
<View style={styles.meta}>
|
|
81
|
-
{hasConfidence ? <Confidence level={props.confidence} score={props.confidenceScore} /> : null}
|
|
82
|
-
{props.rationale ? (
|
|
83
|
-
<Text size="xs" color="muted" style={{ flex: 1 }} numberOfLines={2}>
|
|
84
|
-
{props.rationale}
|
|
85
|
-
</Text>
|
|
86
|
-
) : null}
|
|
87
|
-
</View>
|
|
88
|
-
) : null}
|
|
89
|
-
|
|
90
|
-
<View style={styles.footer}>
|
|
91
|
-
{props.onDismiss ? <Button title="Not a match" color="muted" shape="rounded" onPress={props.onDismiss} /> : null}
|
|
92
|
-
{props.onReassign ? (
|
|
93
|
-
<Button title={hasMatch ? "Reassign" : "Find match"} color="secondary" shape="rounded" onPress={props.onReassign} />
|
|
94
|
-
) : null}
|
|
95
|
-
{hasMatch && props.onAccept ? (
|
|
96
|
-
<Button title={props.acceptLabel ?? "Accept"} color="primary" shape="rounded" onPress={props.onAccept} />
|
|
97
|
-
) : null}
|
|
98
|
-
</View>
|
|
99
|
-
</View>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function Side({ side, right }: { side: MatchSide; right?: boolean }) {
|
|
104
|
-
return (
|
|
105
|
-
<View style={[styles.side, right ? styles.sideRight : null]}>
|
|
106
|
-
<Text size="sm" weight="medium" numberOfLines={1} align={right ? "right" : undefined}>
|
|
107
|
-
{side.title}
|
|
108
|
-
</Text>
|
|
109
|
-
{side.detail ? (
|
|
110
|
-
<Text size="xs" color="muted" numberOfLines={1} align={right ? "right" : undefined}>
|
|
111
|
-
{side.detail}
|
|
112
|
-
</Text>
|
|
113
|
-
) : null}
|
|
114
|
-
</View>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const styles = StyleSheet.create({
|
|
119
|
-
card: {
|
|
120
|
-
borderWidth: 1,
|
|
121
|
-
borderColor: colors.border,
|
|
122
|
-
backgroundColor: colors.white,
|
|
123
|
-
borderRadius: 12,
|
|
124
|
-
padding: 16,
|
|
125
|
-
gap: 12,
|
|
126
|
-
},
|
|
127
|
-
resolved: { backgroundColor: colors.zinc[50] },
|
|
128
|
-
pair: { flexDirection: "row", alignItems: "center", gap: 12 },
|
|
129
|
-
side: { flex: 1, gap: 2 },
|
|
130
|
-
sideRight: { alignItems: "flex-end" },
|
|
131
|
-
meta: { flexDirection: "row", alignItems: "center", gap: 12 },
|
|
132
|
-
footer: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 8 },
|
|
133
|
-
});
|
package/src/record_review.tsx
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { StyleSheet, View } from "react-native";
|
|
2
|
-
import { colors, solid } from "./colors";
|
|
3
|
-
import { Text } from "./text";
|
|
4
|
-
import { Icon } from "./icon";
|
|
5
|
-
import { Button } from "./button";
|
|
6
|
-
import { InlineTextInput } from "./inline_text_input";
|
|
7
|
-
import { Confidence, type ConfidenceLevel } from "./confidence";
|
|
8
|
-
|
|
9
|
-
export interface RecordField {
|
|
10
|
-
label: string;
|
|
11
|
-
value: string;
|
|
12
|
-
/** Unit suffix shown after the value (₫, kg, pcs). */
|
|
13
|
-
unit?: string;
|
|
14
|
-
/** Mark a low-confidence field worth double-checking — a subtle amber dot. */
|
|
15
|
-
uncertain?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface RecordReviewProps {
|
|
19
|
-
/** A short identifier for the record ("Carton 1", a code, a name). */
|
|
20
|
-
title?: string;
|
|
21
|
-
fields: RecordField[];
|
|
22
|
-
confidence?: ConfidenceLevel;
|
|
23
|
-
confidenceScore?: number;
|
|
24
|
-
/** Edit a field — fired on save of the inline editor (blur / Enter). When set,
|
|
25
|
-
* each value is click-to-edit; omit for a read-only review. */
|
|
26
|
-
onEditField?: (index: number, value: string) => void;
|
|
27
|
-
/** Confirm the record — it collapses to a summary the host can re-open. */
|
|
28
|
-
onConfirm?: () => void;
|
|
29
|
-
/** Remove / skip the record (don't add it). */
|
|
30
|
-
onRemove?: () => void;
|
|
31
|
-
/** Re-open a confirmed or removed record. */
|
|
32
|
-
onUndo?: () => void;
|
|
33
|
-
status?: "pending" | "confirmed" | "removed";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* One extracted record under review before it's saved — the agent's fields, each
|
|
38
|
-
* click-to-edit (`InlineTextInput`), with a confidence and Confirm / Remove. On
|
|
39
|
-
* confirm (or remove) the card COLLAPSES to a one-line summary with Undo, so a
|
|
40
|
-
* long extraction reads as a tidy checklist of what's been reviewed. Stack
|
|
41
|
-
* several under a "Save all" to capture a whole document → table; the source +
|
|
42
|
-
* streaming + save belong to the host (see `tpl_extract`). Replaces per-field
|
|
43
|
-
* confirmable estimates with a whole-record review.
|
|
44
|
-
*/
|
|
45
|
-
export function RecordReview(props: RecordReviewProps) {
|
|
46
|
-
const status = props.status ?? "pending";
|
|
47
|
-
const hasConfidence = props.confidence != null || props.confidenceScore != null;
|
|
48
|
-
|
|
49
|
-
if (status === "confirmed" || status === "removed") {
|
|
50
|
-
const removed = status === "removed";
|
|
51
|
-
return (
|
|
52
|
-
<View style={styles.collapsed}>
|
|
53
|
-
<Icon name={removed ? "x" : "check"} size={15} color={removed ? colors.zinc[400] : solid("emerald")} />
|
|
54
|
-
<Text
|
|
55
|
-
size="sm"
|
|
56
|
-
weight="medium"
|
|
57
|
-
color={removed ? "muted" : "default"}
|
|
58
|
-
numberOfLines={1}
|
|
59
|
-
style={[{ flex: 1 }, removed ? styles.strike : null]}
|
|
60
|
-
>
|
|
61
|
-
{props.title ?? props.fields[0]?.value ?? "Record"}
|
|
62
|
-
</Text>
|
|
63
|
-
<Text size="sm" weight="medium" color="muted">
|
|
64
|
-
{removed ? "Removed" : "Confirmed"}
|
|
65
|
-
</Text>
|
|
66
|
-
{props.onUndo ? <Button title="Undo" color="muted" shape="rounded" onPress={props.onUndo} /> : null}
|
|
67
|
-
</View>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<View style={styles.card}>
|
|
73
|
-
<View style={styles.header}>
|
|
74
|
-
<Text size="sm" weight="semibold" style={{ flex: 1 }} numberOfLines={1}>
|
|
75
|
-
{props.title ?? "Record"}
|
|
76
|
-
</Text>
|
|
77
|
-
{hasConfidence ? <Confidence level={props.confidence} score={props.confidenceScore} /> : null}
|
|
78
|
-
</View>
|
|
79
|
-
|
|
80
|
-
<View style={styles.fields}>
|
|
81
|
-
{props.fields.map((f, i) => (
|
|
82
|
-
<View key={`${f.label}-${i}`} style={styles.fieldRow}>
|
|
83
|
-
<View style={styles.labelCol}>
|
|
84
|
-
{f.uncertain ? <View style={styles.uncertainDot} /> : null}
|
|
85
|
-
<Text size="sm" color="muted" numberOfLines={1}>
|
|
86
|
-
{f.label}
|
|
87
|
-
</Text>
|
|
88
|
-
</View>
|
|
89
|
-
<View style={styles.valueCol}>
|
|
90
|
-
<View style={{ flex: 1 }}>
|
|
91
|
-
{props.onEditField ? (
|
|
92
|
-
<InlineTextInput value={f.value} accessibilityLabel={`Edit ${f.label}`} onSave={(next) => props.onEditField?.(i, next)} />
|
|
93
|
-
) : (
|
|
94
|
-
<Text size="sm" weight="medium" tabular>
|
|
95
|
-
{f.value}
|
|
96
|
-
</Text>
|
|
97
|
-
)}
|
|
98
|
-
</View>
|
|
99
|
-
{f.unit ? (
|
|
100
|
-
<Text size="sm" color="muted">
|
|
101
|
-
{f.unit}
|
|
102
|
-
</Text>
|
|
103
|
-
) : null}
|
|
104
|
-
</View>
|
|
105
|
-
</View>
|
|
106
|
-
))}
|
|
107
|
-
</View>
|
|
108
|
-
|
|
109
|
-
{props.onConfirm || props.onRemove ? (
|
|
110
|
-
<View style={styles.footer}>
|
|
111
|
-
{props.onRemove ? <Button title="Remove" color="muted" shape="rounded" onPress={props.onRemove} /> : null}
|
|
112
|
-
{props.onConfirm ? <Button title="Confirm" color="secondary" shape="rounded" onPress={props.onConfirm} /> : null}
|
|
113
|
-
</View>
|
|
114
|
-
) : null}
|
|
115
|
-
</View>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const styles = StyleSheet.create({
|
|
120
|
-
card: {
|
|
121
|
-
borderWidth: 1,
|
|
122
|
-
borderColor: colors.border,
|
|
123
|
-
backgroundColor: colors.white,
|
|
124
|
-
borderRadius: 12,
|
|
125
|
-
padding: 16,
|
|
126
|
-
gap: 12,
|
|
127
|
-
},
|
|
128
|
-
header: { flexDirection: "row", alignItems: "center", gap: 10 },
|
|
129
|
-
fields: { gap: 2 },
|
|
130
|
-
fieldRow: { flexDirection: "row", alignItems: "center", gap: 16, minHeight: 34 },
|
|
131
|
-
labelCol: { flexDirection: "row", alignItems: "center", gap: 6, width: 132 },
|
|
132
|
-
uncertainDot: { width: 6, height: 6, borderRadius: 999, backgroundColor: solid("amber") },
|
|
133
|
-
valueCol: { flex: 1, flexDirection: "row", alignItems: "center", gap: 6 },
|
|
134
|
-
collapsed: {
|
|
135
|
-
flexDirection: "row",
|
|
136
|
-
alignItems: "center",
|
|
137
|
-
gap: 10,
|
|
138
|
-
backgroundColor: colors.white,
|
|
139
|
-
borderWidth: 1,
|
|
140
|
-
borderColor: colors.border,
|
|
141
|
-
borderRadius: 12,
|
|
142
|
-
paddingLeft: 14,
|
|
143
|
-
paddingRight: 8,
|
|
144
|
-
paddingVertical: 6,
|
|
145
|
-
minHeight: 48,
|
|
146
|
-
},
|
|
147
|
-
strike: { textDecorationLine: "line-through" },
|
|
148
|
-
footer: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 8 },
|
|
149
|
-
});
|
package/src/suggestion.tsx
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { ReactNode } from "react";
|
|
2
|
-
import { StyleSheet, View } from "react-native";
|
|
3
|
-
import { colors } from "./colors";
|
|
4
|
-
import { Text } from "./text";
|
|
5
|
-
import { Button } from "./button";
|
|
6
|
-
import { Confidence, type ConfidenceLevel } from "./confidence";
|
|
7
|
-
|
|
8
|
-
export interface SuggestionProps {
|
|
9
|
-
/** The proposal headline — what the AI suggests. */
|
|
10
|
-
title: string;
|
|
11
|
-
/** One line of WHY — the rationale the human is weighing. */
|
|
12
|
-
rationale?: string;
|
|
13
|
-
confidence?: ConfidenceLevel;
|
|
14
|
-
confidenceScore?: number;
|
|
15
|
-
/** The proposal body — render the proposed value/preview however fits (a
|
|
16
|
-
* field stack, a chip row, a diagram). Omit for a title-only suggestion. */
|
|
17
|
-
children?: ReactNode;
|
|
18
|
-
onAccept?: () => void;
|
|
19
|
-
/** Tweak before accepting — the host opens its editor. */
|
|
20
|
-
onEdit?: () => void;
|
|
21
|
-
onDismiss?: () => void;
|
|
22
|
-
acceptLabel?: string;
|
|
23
|
-
/** Once resolved the card settles to a quiet outcome line (no actions). */
|
|
24
|
-
status?: "open" | "accepted" | "dismissed";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The core review-for-approval surface: an AI proposal the human accepts,
|
|
29
|
-
* edits, or dismisses — never auto-applied. A "PROPOSED" eyebrow marks it as
|
|
30
|
-
* the agent's, a `Confidence` meter says how sure, and the rationale is set as a
|
|
31
|
-
* left-ruled margin note — the machine's reasoning, quoted and kept apart from
|
|
32
|
-
* the facts and your controls. The decision is the human's. For a shortlist,
|
|
33
|
-
* stack several (rank by confidence, the top one first). Pair with `AgentRun`
|
|
34
|
-
* (what produced it) and `ChangeReview` (when the proposal edits existing state).
|
|
35
|
-
*/
|
|
36
|
-
export function Suggestion(props: SuggestionProps) {
|
|
37
|
-
const status = props.status ?? "open";
|
|
38
|
-
const hasConfidence = props.confidence != null || props.confidenceScore != null;
|
|
39
|
-
return (
|
|
40
|
-
<View style={[styles.card, status !== "open" ? styles.resolved : null]}>
|
|
41
|
-
<View style={styles.eyebrow}>
|
|
42
|
-
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>
|
|
43
|
-
Proposed
|
|
44
|
-
</Text>
|
|
45
|
-
{status === "open" && hasConfidence ? <Confidence level={props.confidence} score={props.confidenceScore} /> : null}
|
|
46
|
-
</View>
|
|
47
|
-
|
|
48
|
-
<Text size="md" weight="semibold" numberOfLines={2}>
|
|
49
|
-
{props.title}
|
|
50
|
-
</Text>
|
|
51
|
-
|
|
52
|
-
{props.rationale ? (
|
|
53
|
-
<View style={styles.note}>
|
|
54
|
-
<Text size="sm" color="muted">
|
|
55
|
-
{props.rationale}
|
|
56
|
-
</Text>
|
|
57
|
-
</View>
|
|
58
|
-
) : null}
|
|
59
|
-
|
|
60
|
-
{props.children ? <View>{props.children}</View> : null}
|
|
61
|
-
|
|
62
|
-
{status !== "open" ? (
|
|
63
|
-
<Text size="xs" color="muted" weight="medium">
|
|
64
|
-
{status === "accepted" ? "Accepted" : "Dismissed"}
|
|
65
|
-
</Text>
|
|
66
|
-
) : (
|
|
67
|
-
<View style={styles.footer}>
|
|
68
|
-
{props.onDismiss ? <Button title="Dismiss" color="muted" shape="rounded" onPress={props.onDismiss} /> : null}
|
|
69
|
-
{props.onEdit ? <Button title="Edit" color="secondary" shape="rounded" onPress={props.onEdit} /> : null}
|
|
70
|
-
{props.onAccept ? (
|
|
71
|
-
<Button title={props.acceptLabel ?? "Accept"} color="primary" shape="rounded" onPress={props.onAccept} />
|
|
72
|
-
) : null}
|
|
73
|
-
</View>
|
|
74
|
-
)}
|
|
75
|
-
</View>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const styles = StyleSheet.create({
|
|
80
|
-
card: {
|
|
81
|
-
borderWidth: 1,
|
|
82
|
-
borderColor: colors.border,
|
|
83
|
-
backgroundColor: colors.white,
|
|
84
|
-
borderRadius: 12,
|
|
85
|
-
padding: 16,
|
|
86
|
-
gap: 12,
|
|
87
|
-
},
|
|
88
|
-
resolved: { backgroundColor: colors.zinc[50] },
|
|
89
|
-
eyebrow: { flexDirection: "row", alignItems: "center", gap: 8 },
|
|
90
|
-
note: { borderLeftWidth: 2, borderLeftColor: colors.zinc[300], paddingLeft: 12 },
|
|
91
|
-
footer: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 8 },
|
|
92
|
-
});
|