@lotics/ui 4.3.0 → 4.5.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 +98 -28
- 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 +13 -3
- 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/discrepancy.tsx +114 -0
- package/src/finding.tsx +104 -0
- 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/triage_row.tsx +99 -0
- package/src/assisted_field.tsx +0 -93
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
shape="rounded"
|
|
98
|
+
onPress={onSelect}
|
|
99
|
+
/>
|
|
100
|
+
</View>
|
|
101
|
+
) : null}
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const RING: ViewStyle = { boxShadow: `0 0 0 2px ${colors.zinc[900]}` } as ViewStyle;
|
|
107
|
+
|
|
108
|
+
const styles = StyleSheet.create({
|
|
109
|
+
card: {
|
|
110
|
+
borderWidth: 1,
|
|
111
|
+
borderColor: colors.border,
|
|
112
|
+
backgroundColor: colors.white,
|
|
113
|
+
borderRadius: 12,
|
|
114
|
+
padding: 16,
|
|
115
|
+
gap: 12,
|
|
116
|
+
},
|
|
117
|
+
recCard: { borderColor: colors.zinc[400] },
|
|
118
|
+
selected: RING,
|
|
119
|
+
footer: { flexDirection: "row", justifyContent: "flex-end" },
|
|
120
|
+
head: { flexDirection: "row", alignItems: "center", gap: 12 },
|
|
121
|
+
rank: {
|
|
122
|
+
width: 28,
|
|
123
|
+
height: 28,
|
|
124
|
+
borderRadius: 8,
|
|
125
|
+
alignItems: "center",
|
|
126
|
+
justifyContent: "center",
|
|
127
|
+
backgroundColor: colors.zinc[100],
|
|
128
|
+
},
|
|
129
|
+
titleCol: { flex: 1, gap: 1 },
|
|
130
|
+
scoreCol: { alignItems: "flex-end" },
|
|
131
|
+
track: {
|
|
132
|
+
height: 6,
|
|
133
|
+
borderRadius: 999,
|
|
134
|
+
backgroundColor: colors.zinc[100],
|
|
135
|
+
overflow: "hidden",
|
|
136
|
+
},
|
|
137
|
+
fill: { height: "100%", borderRadius: 999, backgroundColor: colors.zinc[900] },
|
|
138
|
+
note: { borderLeftWidth: 2, borderLeftColor: colors.zinc[300], paddingLeft: 10 },
|
|
139
|
+
});
|
package/src/sources.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StyleSheet, View } from "react-native";
|
|
2
|
-
import { colors } from "./colors";
|
|
2
|
+
import { colors, solid, tint, type ColorName } from "./colors";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
import { Icon, type IconName } from "./icon";
|
|
5
5
|
import { PressableHighlight } from "./pressable_highlight";
|
|
@@ -10,7 +10,7 @@ export interface SourceRef {
|
|
|
10
10
|
id: string;
|
|
11
11
|
label: string;
|
|
12
12
|
kind?: SourceKind;
|
|
13
|
-
/** A locator
|
|
13
|
+
/** A locator after the label — "page 2", a record code, a column. */
|
|
14
14
|
detail?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -18,36 +18,41 @@ export interface SourcesProps {
|
|
|
18
18
|
sources: SourceRef[];
|
|
19
19
|
/** Eyebrow above the chips. Default "Sources". */
|
|
20
20
|
label?: string;
|
|
21
|
-
/** Pass to make each chip openable (jump to the record / document / page).
|
|
21
|
+
/** Pass to make each chip openable (jump to the record / document / page). The
|
|
22
|
+
* chips then show a hover state + an open glyph; the host does the navigation. */
|
|
22
23
|
onOpen?: (source: SourceRef) => void;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// Each kind carries its own glyph + colour — provenance you can recognise at a
|
|
27
|
+
// glance (echoing the file-badge palette: document red, table green, …).
|
|
28
|
+
const KIND: Record<SourceKind, { icon: IconName; color: ColorName }> = {
|
|
29
|
+
document: { icon: "file-text", color: "red" },
|
|
30
|
+
table: { icon: "table-2", color: "emerald" },
|
|
31
|
+
record: { icon: "box", color: "blue" },
|
|
32
|
+
knowledge: { icon: "book-open", color: "amber" },
|
|
33
|
+
web: { icon: "globe", color: "sky" },
|
|
31
34
|
};
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Where the AI's output came FROM — a row of source chips under a generated
|
|
35
|
-
* answer, summary, or extracted value, each
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
+
* answer, summary, or extracted value, each with a per-kind coloured glyph.
|
|
39
|
+
* When `onOpen` is set each chip is pressable (hover wash + an open glyph) and
|
|
40
|
+
* the host navigates to the record / document / page. Provenance is what makes
|
|
41
|
+
* AI output verifiable; show it on anything the agent produced from data it read.
|
|
42
|
+
* Renders nothing for an empty list.
|
|
38
43
|
*/
|
|
39
44
|
export function Sources(props: SourcesProps) {
|
|
40
45
|
if (props.sources.length === 0) return null;
|
|
41
46
|
return (
|
|
42
|
-
<View style={{ gap:
|
|
43
|
-
<Text size="xs" color="muted"
|
|
47
|
+
<View style={{ gap: 8 }}>
|
|
48
|
+
<Text size="xs" color="muted" weight="medium">
|
|
44
49
|
{props.label ?? "Sources"}
|
|
45
50
|
</Text>
|
|
46
51
|
<View style={styles.row}>
|
|
47
52
|
{props.sources.map((s) =>
|
|
48
53
|
props.onOpen ? (
|
|
49
|
-
<PressableHighlight key={s.id} onPress={() => props.onOpen?.(s)} style={styles.chip}>
|
|
50
|
-
<SourceChip source={s} />
|
|
54
|
+
<PressableHighlight key={s.id} onPress={() => props.onOpen?.(s)} accessibilityLabel={`Open ${s.label}`} style={styles.chip}>
|
|
55
|
+
<SourceChip source={s} openable />
|
|
51
56
|
</PressableHighlight>
|
|
52
57
|
) : (
|
|
53
58
|
<View key={s.id} style={styles.chip}>
|
|
@@ -60,10 +65,13 @@ export function Sources(props: SourcesProps) {
|
|
|
60
65
|
);
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
function SourceChip({ source }: { source: SourceRef }) {
|
|
68
|
+
function SourceChip({ source, openable }: { source: SourceRef; openable?: boolean }) {
|
|
69
|
+
const k = KIND[source.kind ?? "document"];
|
|
64
70
|
return (
|
|
65
71
|
<>
|
|
66
|
-
<
|
|
72
|
+
<View style={[styles.iconDisc, { backgroundColor: tint(k.color, 0.14) }]}>
|
|
73
|
+
<Icon name={k.icon} size={13} color={solid(k.color)} />
|
|
74
|
+
</View>
|
|
67
75
|
<Text size="xs" weight="medium" numberOfLines={1}>
|
|
68
76
|
{source.label}
|
|
69
77
|
</Text>
|
|
@@ -72,6 +80,7 @@ function SourceChip({ source }: { source: SourceRef }) {
|
|
|
72
80
|
{source.detail}
|
|
73
81
|
</Text>
|
|
74
82
|
) : null}
|
|
83
|
+
{openable ? <Icon name="external-link" size={12} color={colors.zinc[400]} /> : null}
|
|
75
84
|
</>
|
|
76
85
|
);
|
|
77
86
|
}
|
|
@@ -81,12 +90,20 @@ const styles = StyleSheet.create({
|
|
|
81
90
|
chip: {
|
|
82
91
|
flexDirection: "row",
|
|
83
92
|
alignItems: "center",
|
|
84
|
-
gap:
|
|
85
|
-
|
|
93
|
+
gap: 8,
|
|
94
|
+
paddingLeft: 5,
|
|
95
|
+
paddingRight: 10,
|
|
86
96
|
paddingVertical: 5,
|
|
87
|
-
borderRadius:
|
|
97
|
+
borderRadius: 10,
|
|
88
98
|
borderWidth: 1,
|
|
89
99
|
borderColor: colors.border,
|
|
90
100
|
backgroundColor: colors.white,
|
|
91
101
|
},
|
|
102
|
+
iconDisc: {
|
|
103
|
+
width: 24,
|
|
104
|
+
height: 24,
|
|
105
|
+
borderRadius: 7,
|
|
106
|
+
alignItems: "center",
|
|
107
|
+
justifyContent: "center",
|
|
108
|
+
},
|
|
92
109
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
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/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
|
});
|
|
@@ -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
|
-
});
|