@lotics/ui 4.4.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.
@@ -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 under the label — "page 2", a record code, a column. */
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
- const KIND_ICON: Record<SourceKind, IconName> = {
26
- record: "box",
27
- document: "file-text",
28
- table: "table-2",
29
- web: "external-link",
30
- knowledge: "book-open",
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 openable. Provenance is what makes
36
- * AI output verifiable; show it on anything the agent produced from data it
37
- * read (extraction, summarization, Q&A). Renders nothing for an empty list.
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: 6 }}>
43
- <Text size="xs" color="muted" transform="uppercase">
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
- <Icon name={KIND_ICON[source.kind ?? "document"]} size={12} color={colors.zinc[500]} />
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: 6,
85
- paddingHorizontal: 10,
93
+ gap: 8,
94
+ paddingLeft: 5,
95
+ paddingRight: 10,
86
96
  paddingVertical: 5,
87
- borderRadius: 8,
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
+ });
@@ -1,10 +1,8 @@
1
1
  import { ReactNode } from "react";
2
2
  import { StyleSheet, View } from "react-native";
3
- import { colors, solid, tint } from "./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. The violet sparkle marks it as
32
- * AI-proposed; a `Confidence` chip says how sure; the body shows the proposed
33
- * value. The decision is the human's. For a shortlist, stack several (rank by
34
- * confidence, the top one first). Pair with `AgentRun` (what produced it) and
35
- * `ChangeReview` (when the proposal is an edit to existing state).
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.header}>
43
- <Icon name="sparkles" size={14} color={solid("violet")} />
44
- <Text size="sm" weight="semibold" style={{ flex: 1 }} numberOfLines={2}>
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
- <Text size="xs" color="muted">
54
- {props.rationale}
55
- </Text>
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
- <View style={styles.outcome}>
62
- <Icon
63
- name={status === "accepted" ? "circle-check" : "x"}
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 ? <LinkButton title="Dismiss" onPress={props.onDismiss} /> : <View />}
74
- <View style={styles.actions}>
75
- {props.onEdit ? (
76
- <Button title="Edit" color="secondary" shape="rounded" icon="square-pen" onPress={props.onEdit} />
77
- ) : null}
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: tint("violet", 0.35),
82
+ borderColor: colors.border,
92
83
  backgroundColor: colors.white,
93
84
  borderRadius: 12,
94
- padding: 14,
95
- gap: 10,
85
+ padding: 16,
86
+ gap: 12,
96
87
  },
97
- resolved: { borderColor: colors.border, backgroundColor: colors.zinc[50] },
98
- header: { flexDirection: "row", alignItems: "center", gap: 8 },
99
- outcome: { flexDirection: "row", alignItems: "center", gap: 6 },
100
- footer: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" },
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
+ });
@@ -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
- });