@lotics/ui 4.4.0 → 4.6.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 +108 -31
- package/examples/tpl_assistant.tsx +6 -6
- package/examples/tpl_briefing.tsx +121 -0
- package/examples/tpl_compare.tsx +133 -0
- package/examples/tpl_crosscheck.tsx +120 -0
- package/examples/tpl_dieline.tsx +218 -55
- package/examples/tpl_draft.tsx +7 -9
- package/examples/tpl_extract.tsx +91 -92
- package/examples/tpl_lookup.tsx +288 -0
- package/examples/tpl_match.tsx +123 -0
- package/examples/tpl_triage.tsx +112 -0
- package/package.json +9 -2
- package/src/agent_progress.tsx +35 -23
- package/src/agent_run.tsx +60 -80
- package/src/change_review.tsx +190 -110
- package/src/choice_list.tsx +63 -0
- package/src/clarify.tsx +19 -39
- package/src/confidence.tsx +30 -8
- package/src/date_filter.tsx +3 -11
- package/src/date_range_filter_field.tsx +38 -27
- package/src/discrepancy.tsx +114 -0
- package/src/finding.tsx +104 -0
- package/src/format_date.test.ts +32 -4
- package/src/format_date.ts +61 -23
- package/src/inline_date_picker.tsx +3 -1
- package/src/match_row.tsx +133 -0
- package/src/prompt_field.tsx +47 -89
- package/src/record_review.tsx +149 -0
- package/src/scored_option.tsx +139 -0
- package/src/sources.tsx +38 -21
- package/src/spec_list.tsx +81 -0
- package/src/suggestion.tsx +35 -45
- package/src/time_field.tsx +4 -5
- package/src/triage_row.tsx +99 -0
- package/src/assisted_field.tsx +0 -93
package/src/suggestion.tsx
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
2
|
import { StyleSheet, View } from "react-native";
|
|
3
|
-
import { colors
|
|
3
|
+
import { colors } from "./colors";
|
|
4
4
|
import { Text } from "./text";
|
|
5
|
-
import { Icon } from "./icon";
|
|
6
5
|
import { Button } from "./button";
|
|
7
|
-
import { LinkButton } from "./link_button";
|
|
8
6
|
import { Confidence, type ConfidenceLevel } from "./confidence";
|
|
9
7
|
|
|
10
8
|
export interface SuggestionProps {
|
|
@@ -28,57 +26,50 @@ export interface SuggestionProps {
|
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
28
|
* The core review-for-approval surface: an AI proposal the human accepts,
|
|
31
|
-
* edits, or dismisses — never auto-applied.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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).
|
|
36
35
|
*/
|
|
37
36
|
export function Suggestion(props: SuggestionProps) {
|
|
38
37
|
const status = props.status ?? "open";
|
|
39
38
|
const hasConfidence = props.confidence != null || props.confidenceScore != null;
|
|
40
39
|
return (
|
|
41
40
|
<View style={[styles.card, status !== "open" ? styles.resolved : null]}>
|
|
42
|
-
<View style={styles.
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
{props.title}
|
|
41
|
+
<View style={styles.eyebrow}>
|
|
42
|
+
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>
|
|
43
|
+
Proposed
|
|
46
44
|
</Text>
|
|
47
|
-
{status === "open" && hasConfidence ?
|
|
48
|
-
<Confidence level={props.confidence} score={props.confidenceScore} />
|
|
49
|
-
) : null}
|
|
45
|
+
{status === "open" && hasConfidence ? <Confidence level={props.confidence} score={props.confidenceScore} /> : null}
|
|
50
46
|
</View>
|
|
51
47
|
|
|
48
|
+
<Text size="md" weight="semibold" numberOfLines={2}>
|
|
49
|
+
{props.title}
|
|
50
|
+
</Text>
|
|
51
|
+
|
|
52
52
|
{props.rationale ? (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
<View style={styles.note}>
|
|
54
|
+
<Text size="sm" color="muted">
|
|
55
|
+
{props.rationale}
|
|
56
|
+
</Text>
|
|
57
|
+
</View>
|
|
56
58
|
) : null}
|
|
57
59
|
|
|
58
60
|
{props.children ? <View>{props.children}</View> : null}
|
|
59
61
|
|
|
60
62
|
{status !== "open" ? (
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
size={14}
|
|
65
|
-
color={status === "accepted" ? solid("emerald") : colors.zinc[400]}
|
|
66
|
-
/>
|
|
67
|
-
<Text size="xs" color="muted">
|
|
68
|
-
{status === "accepted" ? "Accepted" : "Dismissed"}
|
|
69
|
-
</Text>
|
|
70
|
-
</View>
|
|
63
|
+
<Text size="xs" color="muted" weight="medium">
|
|
64
|
+
{status === "accepted" ? "Accepted" : "Dismissed"}
|
|
65
|
+
</Text>
|
|
71
66
|
) : (
|
|
72
67
|
<View style={styles.footer}>
|
|
73
|
-
{props.onDismiss ? <
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{props.onAccept ? (
|
|
79
|
-
<Button title={props.acceptLabel ?? "Accept"} color="primary" shape="rounded" onPress={props.onAccept} />
|
|
80
|
-
) : null}
|
|
81
|
-
</View>
|
|
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}
|
|
82
73
|
</View>
|
|
83
74
|
)}
|
|
84
75
|
</View>
|
|
@@ -88,15 +79,14 @@ export function Suggestion(props: SuggestionProps) {
|
|
|
88
79
|
const styles = StyleSheet.create({
|
|
89
80
|
card: {
|
|
90
81
|
borderWidth: 1,
|
|
91
|
-
borderColor:
|
|
82
|
+
borderColor: colors.border,
|
|
92
83
|
backgroundColor: colors.white,
|
|
93
84
|
borderRadius: 12,
|
|
94
|
-
padding:
|
|
95
|
-
gap:
|
|
85
|
+
padding: 16,
|
|
86
|
+
gap: 12,
|
|
96
87
|
},
|
|
97
|
-
resolved: {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
footer: { flexDirection: "row", alignItems: "center", justifyContent: "
|
|
101
|
-
actions: { flexDirection: "row", alignItems: "center", gap: 8 },
|
|
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 },
|
|
102
92
|
});
|
package/src/time_field.tsx
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
fieldOrder,
|
|
11
11
|
from12h,
|
|
12
12
|
getTimeLayout,
|
|
13
|
-
placeholderFor,
|
|
14
13
|
to12h,
|
|
15
14
|
} from "./date_segments";
|
|
16
15
|
|
|
@@ -119,13 +118,13 @@ export function TimeField(props: TimeFieldProps) {
|
|
|
119
118
|
"hour" | "minute" | "dayPeriod",
|
|
120
119
|
{ options: Option[]; onSelect: (v: string) => void; label: string; width: number }
|
|
121
120
|
> = {
|
|
122
|
-
hour: { options: hourOptions, onSelect: setHour, label: segmentLabels.hour, width:
|
|
123
|
-
minute: { options: minuteOptions, onSelect: setMinute, label: segmentLabels.minute, width:
|
|
121
|
+
hour: { options: hourOptions, onSelect: setHour, label: segmentLabels.hour, width: 78 },
|
|
122
|
+
minute: { options: minuteOptions, onSelect: setMinute, label: segmentLabels.minute, width: 78 },
|
|
124
123
|
dayPeriod: {
|
|
125
124
|
options: periodOptions,
|
|
126
125
|
onSelect: setPeriod,
|
|
127
126
|
label: segmentLabels.dayPeriod,
|
|
128
|
-
width:
|
|
127
|
+
width: 78,
|
|
129
128
|
},
|
|
130
129
|
};
|
|
131
130
|
|
|
@@ -142,7 +141,7 @@ export function TimeField(props: TimeFieldProps) {
|
|
|
142
141
|
options={c.options}
|
|
143
142
|
onSelect={c.onSelect}
|
|
144
143
|
accessibilityLabel={c.label}
|
|
145
|
-
placeholder={
|
|
144
|
+
placeholder={c.label}
|
|
146
145
|
disabled={disabled}
|
|
147
146
|
width={c.width}
|
|
148
147
|
/>
|
|
@@ -0,0 +1,99 @@
|
|
|
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" shape="rounded" onPress={props.onOverride} /> : null}
|
|
78
|
+
{props.onDismiss ? <Button title="Dismiss" color="muted" shape="rounded" onPress={props.onDismiss} /> : null}
|
|
79
|
+
{props.onAccept ? <Button title="Accept" color="primary" shape="rounded" 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
|
+
});
|
package/src/assisted_field.tsx
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { StyleSheet, View } from "react-native";
|
|
2
|
-
import { solid, tint } from "./colors";
|
|
3
|
-
import { Text } from "./text";
|
|
4
|
-
import { Icon } from "./icon";
|
|
5
|
-
import { IconButton } from "./icon_button";
|
|
6
|
-
import { LinkButton } from "./link_button";
|
|
7
|
-
import { Confidence, type ConfidenceLevel } from "./confidence";
|
|
8
|
-
|
|
9
|
-
export interface AssistedFieldProps {
|
|
10
|
-
label: string;
|
|
11
|
-
/** The current value — an AI estimate until confirmed, or the final value. */
|
|
12
|
-
value: string;
|
|
13
|
-
/** Unit suffix shown after the value (mm, cm, kg). */
|
|
14
|
-
unit?: string;
|
|
15
|
-
confidence?: ConfidenceLevel;
|
|
16
|
-
confidenceScore?: number;
|
|
17
|
-
/** True while the value is still an unconfirmed AI estimate — it reads
|
|
18
|
-
* amber-tinted with a Confidence chip and a Confirm action (visible
|
|
19
|
-
* provenance). Confirm or override flips it to a plain confirmed value. */
|
|
20
|
-
estimated?: boolean;
|
|
21
|
-
onConfirm?: () => void;
|
|
22
|
-
/** Override — the host swaps in its own inline input to edit the value. */
|
|
23
|
-
onEdit?: () => void;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* A form value the AI pre-filled — shown as a tinted ESTIMATE the human
|
|
28
|
-
* confirms (→ plain, checked) or overrides, never silently committed. The
|
|
29
|
-
* provenance is always visible: an estimate looks different from a value a
|
|
30
|
-
* person stood behind. Pair with `Confidence`; for a whole extracted record,
|
|
31
|
-
* stack several and add one "Confirm all" at the form level.
|
|
32
|
-
*/
|
|
33
|
-
export function AssistedField(props: AssistedFieldProps) {
|
|
34
|
-
const { label, value, unit, estimated, confidence, confidenceScore, onConfirm, onEdit } = props;
|
|
35
|
-
const display = unit ? `${value} ${unit}` : value;
|
|
36
|
-
return (
|
|
37
|
-
<View style={styles.row}>
|
|
38
|
-
<Text size="sm" color="muted" style={styles.label}>
|
|
39
|
-
{label}
|
|
40
|
-
</Text>
|
|
41
|
-
<View style={styles.valueCol}>
|
|
42
|
-
<View style={styles.valueRow}>
|
|
43
|
-
{estimated ? (
|
|
44
|
-
<View style={styles.estimatePill}>
|
|
45
|
-
<Icon name="sparkles" size={11} color={solid("violet")} />
|
|
46
|
-
<Text size="sm" weight="medium" tabular>
|
|
47
|
-
{display}
|
|
48
|
-
</Text>
|
|
49
|
-
</View>
|
|
50
|
-
) : (
|
|
51
|
-
<View style={styles.confirmedRow}>
|
|
52
|
-
<Text size="sm" weight="medium" tabular>
|
|
53
|
-
{display}
|
|
54
|
-
</Text>
|
|
55
|
-
<Icon name="circle-check" size={13} color={solid("emerald")} />
|
|
56
|
-
</View>
|
|
57
|
-
)}
|
|
58
|
-
{onEdit ? (
|
|
59
|
-
<IconButton icon="square-pen" size="sm" color="none" accessibilityLabel={`Edit ${label}`} onPress={onEdit} />
|
|
60
|
-
) : null}
|
|
61
|
-
</View>
|
|
62
|
-
{estimated ? (
|
|
63
|
-
<View style={styles.estimateMeta}>
|
|
64
|
-
{confidence != null || confidenceScore != null ? (
|
|
65
|
-
<Confidence level={confidence} score={confidenceScore} />
|
|
66
|
-
) : null}
|
|
67
|
-
{onConfirm ? <LinkButton title="Confirm" onPress={onConfirm} /> : null}
|
|
68
|
-
</View>
|
|
69
|
-
) : null}
|
|
70
|
-
</View>
|
|
71
|
-
</View>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const styles = StyleSheet.create({
|
|
76
|
-
row: { flexDirection: "row", alignItems: "flex-start", gap: 16, paddingVertical: 8 },
|
|
77
|
-
label: { width: 132, paddingTop: 6 },
|
|
78
|
-
valueCol: { flex: 1, gap: 6, alignItems: "flex-end" },
|
|
79
|
-
valueRow: { flexDirection: "row", alignItems: "center", gap: 6 },
|
|
80
|
-
estimatePill: {
|
|
81
|
-
flexDirection: "row",
|
|
82
|
-
alignItems: "center",
|
|
83
|
-
gap: 6,
|
|
84
|
-
paddingHorizontal: 10,
|
|
85
|
-
paddingVertical: 5,
|
|
86
|
-
borderRadius: 8,
|
|
87
|
-
backgroundColor: tint("amber", 0.12),
|
|
88
|
-
borderWidth: 1,
|
|
89
|
-
borderColor: tint("amber", 0.35),
|
|
90
|
-
},
|
|
91
|
-
confirmedRow: { flexDirection: "row", alignItems: "center", gap: 6, paddingVertical: 5 },
|
|
92
|
-
estimateMeta: { flexDirection: "row", alignItems: "center", gap: 10 },
|
|
93
|
-
});
|