@lotics/ui 4.2.0 → 4.4.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,92 @@
1
+ import { StyleSheet, View } from "react-native";
2
+ import { colors } from "./colors";
3
+ import { Text } from "./text";
4
+ import { Icon, type IconName } from "./icon";
5
+ import { PressableHighlight } from "./pressable_highlight";
6
+
7
+ export type SourceKind = "record" | "document" | "table" | "web" | "knowledge";
8
+
9
+ export interface SourceRef {
10
+ id: string;
11
+ label: string;
12
+ kind?: SourceKind;
13
+ /** A locator under the label — "page 2", a record code, a column. */
14
+ detail?: string;
15
+ }
16
+
17
+ export interface SourcesProps {
18
+ sources: SourceRef[];
19
+ /** Eyebrow above the chips. Default "Sources". */
20
+ label?: string;
21
+ /** Pass to make each chip openable (jump to the record / document / page). */
22
+ onOpen?: (source: SourceRef) => void;
23
+ }
24
+
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",
31
+ };
32
+
33
+ /**
34
+ * 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
+ */
39
+ export function Sources(props: SourcesProps) {
40
+ if (props.sources.length === 0) return null;
41
+ return (
42
+ <View style={{ gap: 6 }}>
43
+ <Text size="xs" color="muted" transform="uppercase">
44
+ {props.label ?? "Sources"}
45
+ </Text>
46
+ <View style={styles.row}>
47
+ {props.sources.map((s) =>
48
+ props.onOpen ? (
49
+ <PressableHighlight key={s.id} onPress={() => props.onOpen?.(s)} style={styles.chip}>
50
+ <SourceChip source={s} />
51
+ </PressableHighlight>
52
+ ) : (
53
+ <View key={s.id} style={styles.chip}>
54
+ <SourceChip source={s} />
55
+ </View>
56
+ ),
57
+ )}
58
+ </View>
59
+ </View>
60
+ );
61
+ }
62
+
63
+ function SourceChip({ source }: { source: SourceRef }) {
64
+ return (
65
+ <>
66
+ <Icon name={KIND_ICON[source.kind ?? "document"]} size={12} color={colors.zinc[500]} />
67
+ <Text size="xs" weight="medium" numberOfLines={1}>
68
+ {source.label}
69
+ </Text>
70
+ {source.detail ? (
71
+ <Text size="xs" color="muted" numberOfLines={1}>
72
+ {source.detail}
73
+ </Text>
74
+ ) : null}
75
+ </>
76
+ );
77
+ }
78
+
79
+ const styles = StyleSheet.create({
80
+ row: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
81
+ chip: {
82
+ flexDirection: "row",
83
+ alignItems: "center",
84
+ gap: 6,
85
+ paddingHorizontal: 10,
86
+ paddingVertical: 5,
87
+ borderRadius: 8,
88
+ borderWidth: 1,
89
+ borderColor: colors.border,
90
+ backgroundColor: colors.white,
91
+ },
92
+ });
@@ -0,0 +1,102 @@
1
+ import { ReactNode } from "react";
2
+ import { StyleSheet, View } from "react-native";
3
+ import { colors, solid, tint } from "./colors";
4
+ import { Text } from "./text";
5
+ import { Icon } from "./icon";
6
+ import { Button } from "./button";
7
+ import { LinkButton } from "./link_button";
8
+ import { Confidence, type ConfidenceLevel } from "./confidence";
9
+
10
+ export interface SuggestionProps {
11
+ /** The proposal headline — what the AI suggests. */
12
+ title: string;
13
+ /** One line of WHY — the rationale the human is weighing. */
14
+ rationale?: string;
15
+ confidence?: ConfidenceLevel;
16
+ confidenceScore?: number;
17
+ /** The proposal body — render the proposed value/preview however fits (a
18
+ * field stack, a chip row, a diagram). Omit for a title-only suggestion. */
19
+ children?: ReactNode;
20
+ onAccept?: () => void;
21
+ /** Tweak before accepting — the host opens its editor. */
22
+ onEdit?: () => void;
23
+ onDismiss?: () => void;
24
+ acceptLabel?: string;
25
+ /** Once resolved the card settles to a quiet outcome line (no actions). */
26
+ status?: "open" | "accepted" | "dismissed";
27
+ }
28
+
29
+ /**
30
+ * 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).
36
+ */
37
+ export function Suggestion(props: SuggestionProps) {
38
+ const status = props.status ?? "open";
39
+ const hasConfidence = props.confidence != null || props.confidenceScore != null;
40
+ return (
41
+ <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}
46
+ </Text>
47
+ {status === "open" && hasConfidence ? (
48
+ <Confidence level={props.confidence} score={props.confidenceScore} />
49
+ ) : null}
50
+ </View>
51
+
52
+ {props.rationale ? (
53
+ <Text size="xs" color="muted">
54
+ {props.rationale}
55
+ </Text>
56
+ ) : null}
57
+
58
+ {props.children ? <View>{props.children}</View> : null}
59
+
60
+ {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>
71
+ ) : (
72
+ <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>
82
+ </View>
83
+ )}
84
+ </View>
85
+ );
86
+ }
87
+
88
+ const styles = StyleSheet.create({
89
+ card: {
90
+ borderWidth: 1,
91
+ borderColor: tint("violet", 0.35),
92
+ backgroundColor: colors.white,
93
+ borderRadius: 12,
94
+ padding: 14,
95
+ gap: 10,
96
+ },
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 },
102
+ });