@lotics/ui 7.19.3 → 8.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.
@@ -1,121 +0,0 @@
1
- import { useState } from "react";
2
- import { ScrollView, View } from "react-native";
3
- import { Text } from "@lotics/ui/text";
4
- import { colors } from "@lotics/ui/colors";
5
- import { KPIStrip } from "@lotics/ui/kpi_strip";
6
- import { SegmentedControl } from "@lotics/ui/segmented_control";
7
- import { Sources } from "@lotics/ui/sources";
8
- import { Finding, type FindingSeverity } from "@lotics/ui/finding";
9
-
10
- // ─────────────────────────────────────────────────────────────────────────────
11
- // Template · Briefing — the agent reads the period's data and writes a short
12
- // narrative, then a RANKED list of what needs attention. Each finding carries a
13
- // severity, a headline metric, the sources it rests on, and the one action it
14
- // suggests. Unlike a dashboard (raw charts), this is the agent's COMMENTARY:
15
- // what changed, what's at risk, what to do. The leadership digest / daily ops
16
- // brief / anomaly scan. The narrative explains; the findings are the worklist.
17
- // ─────────────────────────────────────────────────────────────────────────────
18
-
19
- interface F {
20
- severity: FindingSeverity;
21
- title: string;
22
- detail: string;
23
- metric: string;
24
- metricCaption: string;
25
- action: string;
26
- sources: { id: string; label: string; kind: "record" | "table" | "document" }[];
27
- }
28
-
29
- const NARRATIVE: Record<"today" | "week", string> = {
30
- today:
31
- "Volume held steady at 41 active shipments. Three reefer containers to Hamburg are inside the demurrage window and need a pickup booked today. Cash collection is lagging — five invoices crossed 30 days overdue. On-time delivery improved on the back of the new Cát Lái slotting.",
32
- week:
33
- "A solid week: 268 shipments moved, on-time delivery up 6 points after the Cát Lái slotting change. Two ocean lanes slipped below the 12% margin target on bunker surcharges. Receivables remain the soft spot — ₫120M is now past 30 days, concentrated in two accounts.",
34
- };
35
-
36
- const FINDINGS: Record<"today" | "week", F[]> = {
37
- today: [
38
- { severity: "critical", title: "3 reefer containers at demurrage risk", detail: "Hamburg shipments HBL-4471/4472/4480 hit free-time expiry at 18:00 today; no pickup is booked.", metric: "₫48M", metricCaption: "exposure", action: "Open the shipments", sources: [{ id: "a1", label: "SHIP-4471", kind: "record" }, { id: "a2", label: "Demurrage tracker", kind: "table" }] },
39
- { severity: "warning", title: "5 invoices overdue past 30 days", detail: "Two accounts — Crestline and Meridian — make up most of the balance. No promise-to-pay logged this week.", metric: "₫120M", metricCaption: "overdue", action: "Open receivables", sources: [{ id: "a3", label: "Aged receivables", kind: "table" }] },
40
- { severity: "warning", title: "Margin on the Hamburg lane below target", detail: "Bunker surcharge rose 9%; the lane is at 9.8% gross vs the 12% floor.", metric: "−2.2 pts", metricCaption: "vs target", action: "Review lane pricing", sources: [{ id: "a4", label: "Lane P&L", kind: "table" }] },
41
- { severity: "positive", title: "On-time delivery improving", detail: "The Cát Lái slotting change lifted on-time pickups for the third day running.", metric: "+6 pts", metricCaption: "3-day", action: "See the trend", sources: [{ id: "a5", label: "OTD report", kind: "document" }] },
42
- ],
43
- week: [
44
- { severity: "critical", title: "Receivables concentration risk", detail: "₫120M past 30 days sits in two accounts; one is also near its credit limit.", metric: "2", metricCaption: "accounts", action: "Open receivables", sources: [{ id: "b1", label: "Aged receivables", kind: "table" }] },
45
- { severity: "warning", title: "Two ocean lanes below margin target", detail: "Hamburg and Rotterdam slipped under 12% on bunker surcharges sustained all week.", metric: "2", metricCaption: "lanes", action: "Review lane pricing", sources: [{ id: "b2", label: "Lane P&L", kind: "table" }] },
46
- { severity: "positive", title: "On-time delivery up 6 points", detail: "The slotting change held all week — the strongest OTD in two months.", metric: "+6 pts", metricCaption: "this week", action: "See the trend", sources: [{ id: "b3", label: "OTD report", kind: "document" }] },
47
- { severity: "info", title: "RFQ volume rising", detail: "12 new quote requests this week, ahead of the trailing average — capacity planning should get ahead of it.", metric: "12", metricCaption: "RFQs", action: "Open the pipeline", sources: [{ id: "b4", label: "Quote pipeline", kind: "table" }] },
48
- ],
49
- };
50
-
51
- export function TplBriefing() {
52
- const [period, setPeriod] = useState<"today" | "week">("today");
53
- const findings = FINDINGS[period];
54
- const attention = findings.filter((f) => f.severity === "critical" || f.severity === "warning").length;
55
-
56
- return (
57
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
58
- <View style={{ width: "100%", maxWidth: 860, alignSelf: "center", gap: 16 }}>
59
- <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
60
- <View style={{ flex: 1, gap: 2 }}>
61
- <Text size="xl" weight="semibold">Operations briefing</Text>
62
- <Text size="sm" color="muted">What the agent sees in the numbers — and what needs a decision</Text>
63
- </View>
64
- <SegmentedControl
65
- accessibilityLabel="Period"
66
- options={[
67
- { label: "Today", value: "today" },
68
- { label: "This week", value: "week" },
69
- ]}
70
- value={period}
71
- onValueChange={setPeriod}
72
- />
73
- </View>
74
-
75
- {/* the generated narrative */}
76
- <View style={{ borderWidth: 1, borderColor: colors.zinc[200], backgroundColor: colors.white, borderRadius: 14, padding: 18, gap: 12 }}>
77
- <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
78
- <Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Summary</Text>
79
- <Text size="xs" color="muted">{period === "today" ? "as of 14:20" : "week to date"}</Text>
80
- </View>
81
- <Text size="md" style={{ lineHeight: 22 }}>{NARRATIVE[period]}</Text>
82
- <Sources
83
- label="Read from"
84
- onOpen={() => {}}
85
- sources={[
86
- { id: "n1", label: "Shipments", kind: "table" },
87
- { id: "n2", label: "Aged receivables", kind: "table" },
88
- { id: "n3", label: "Lane P&L", kind: "table" },
89
- ]}
90
- />
91
- </View>
92
-
93
- <KPIStrip
94
- items={[
95
- { label: "Active shipments", value: period === "today" ? 41 : 268, format: "number", trend: period === "today" ? 0 : 4 },
96
- { label: "On-time delivery", value: "94%", trend: 6 },
97
- { label: "Overdue receivables", value: 120_000_000, format: "currency", compact: true, tone: "danger" },
98
- { label: "Needs attention", value: attention, format: "number", tone: attention > 0 ? "warning" : "default" },
99
- ]}
100
- />
101
-
102
- <View style={{ gap: 10 }}>
103
- <Text size="sm" weight="semibold">Needs attention</Text>
104
- {findings.map((f, i) => (
105
- <Finding
106
- key={i}
107
- severity={f.severity}
108
- title={f.title}
109
- detail={f.detail}
110
- metric={f.metric}
111
- metricCaption={f.metricCaption}
112
- sources={f.sources}
113
- onOpenSource={() => {}}
114
- action={{ label: f.action, onPress: () => {} }}
115
- />
116
- ))}
117
- </View>
118
- </View>
119
- </ScrollView>
120
- );
121
- }
@@ -1,133 +0,0 @@
1
- import { useState } from "react";
2
- import { ScrollView, View } from "react-native";
3
- import { Text } from "@lotics/ui/text";
4
- import { colors } from "@lotics/ui/colors";
5
- import { Callout, CalloutText, CalloutTitle } from "@lotics/ui/callout";
6
- import { ScoredOption } from "@lotics/ui/scored_option";
7
- import type { SpecRow } from "@lotics/ui/spec_list";
8
-
9
- // ─────────────────────────────────────────────────────────────────────────────
10
- // Template · Compare — the agent SCORES and ranks the options against the
11
- // criteria, shows its reasoning and the key specs, and the human picks one. The
12
- // agent does the legwork (gather, normalise, score); the choice stays the
13
- // human's. Quotes, carriers, suppliers, plans, routes. The recommended option
14
- // wears a RECOMMENDED label; selecting one settles the shortlist to the decision.
15
- // ─────────────────────────────────────────────────────────────────────────────
16
-
17
- interface O {
18
- id: string;
19
- rank: number;
20
- title: string;
21
- subtitle: string;
22
- score: number;
23
- rationale: string;
24
- specs: SpecRow[];
25
- recommended?: boolean;
26
- }
27
-
28
- const OPTIONS: O[] = [
29
- {
30
- id: "o1",
31
- rank: 1,
32
- title: "Maersk — Cát Lái → Hamburg",
33
- subtitle: "Direct, weekly service · reefer guaranteed",
34
- score: 0.92,
35
- rationale: "Best balance: only 4% above the cheapest, but the most reliable schedule and guaranteed reefer plugs — lowest demurrage risk for time-sensitive cargo.",
36
- recommended: true,
37
- specs: [
38
- { label: "All-in rate", value: "$2,840" },
39
- { label: "Transit", value: "28 days" },
40
- { label: "Schedule reliability", value: "94%" },
41
- { label: "Free time", value: "14 days" },
42
- ],
43
- },
44
- {
45
- id: "o2",
46
- rank: 2,
47
- title: "CMA CGM — Cát Lái → Hamburg",
48
- subtitle: "1 transshipment at Singapore",
49
- score: 0.78,
50
- rationale: "Cheapest of the four, but the Singapore transshipment adds three days and a reliability hit — workable for non-urgent cargo.",
51
- specs: [
52
- { label: "All-in rate", value: "$2,730" },
53
- { label: "Transit", value: "31 days" },
54
- { label: "Schedule reliability", value: "86%" },
55
- { label: "Free time", value: "10 days" },
56
- ],
57
- },
58
- {
59
- id: "o3",
60
- rank: 3,
61
- title: "Hapag-Lloyd — Cát Lái → Hamburg",
62
- subtitle: "Direct, premium service",
63
- score: 0.64,
64
- rationale: "Fastest transit and strong reliability, but the premium rate is hard to justify unless the customer pays for speed.",
65
- specs: [
66
- { label: "All-in rate", value: "$3,210" },
67
- { label: "Transit", value: "26 days" },
68
- { label: "Schedule reliability", value: "92%" },
69
- { label: "Free time", value: "10 days" },
70
- ],
71
- },
72
- {
73
- id: "o4",
74
- rank: 4,
75
- title: "ONE — Cát Lái → Hamburg",
76
- subtitle: "2 transshipments · no reefer guarantee",
77
- score: 0.41,
78
- rationale: "Lowest score: two transshipments and no guaranteed reefer plug make it unsuitable for frozen cargo, despite a mid-range rate.",
79
- specs: [
80
- { label: "All-in rate", value: "$2,910" },
81
- { label: "Transit", value: "34 days" },
82
- { label: "Schedule reliability", value: "79%" },
83
- { label: "Free time", value: "7 days" },
84
- ],
85
- },
86
- ];
87
-
88
- export function TplCompare() {
89
- const [chosen, setChosen] = useState<string | null>(null);
90
- const pick = OPTIONS.find((o) => o.id === chosen) ?? null;
91
-
92
- return (
93
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
94
- <View style={{ width: "100%", maxWidth: 720, alignSelf: "center", gap: 16 }}>
95
- <View style={{ gap: 2 }}>
96
- <Text size="xl" weight="semibold">Freight options</Text>
97
- <Text size="sm" color="muted">Ocean · Cát Lái → Hamburg · 2×40HC reefer · frozen shrimp</Text>
98
- </View>
99
-
100
- <View style={{ borderLeftWidth: 2, borderLeftColor: colors.zinc[300], paddingLeft: 12 }}>
101
- <Text size="sm" color="muted">
102
- The agent scored four quotes against cost, transit time and schedule reliability, weighted for time-sensitive reefer cargo.
103
- </Text>
104
- </View>
105
-
106
- {pick ? (
107
- <Callout tone="neutral">
108
- <CalloutTitle>Chosen · {pick.title}</CalloutTitle>
109
- <CalloutText>{pick.specs[0].label} {String(pick.specs[0].value)} · {String(pick.specs[1].value)} transit. Booking can proceed on this option.</CalloutText>
110
- </Callout>
111
- ) : null}
112
-
113
- <View style={{ gap: 12 }}>
114
- {OPTIONS.map((o) => (
115
- <ScoredOption
116
- key={o.id}
117
- rank={o.rank}
118
- title={o.title}
119
- subtitle={o.subtitle}
120
- score={o.score}
121
- rationale={o.rationale}
122
- specs={o.specs}
123
- recommended={o.recommended}
124
- selected={chosen === o.id}
125
- selectLabel="Choose this option"
126
- onSelect={() => setChosen((c) => (c === o.id ? null : o.id))}
127
- />
128
- ))}
129
- </View>
130
- </View>
131
- </ScrollView>
132
- );
133
- }
@@ -1,120 +0,0 @@
1
- import { useState } from "react";
2
- import { ScrollView, View } from "react-native";
3
- import { Text } from "@lotics/ui/text";
4
- import { colors } from "@lotics/ui/colors";
5
- import { KPIStrip } from "@lotics/ui/kpi_strip";
6
- import { Callout, CalloutText, CalloutTitle } from "@lotics/ui/callout";
7
- import { Discrepancy, type DiscrepancyValue } from "@lotics/ui/discrepancy";
8
-
9
- // ─────────────────────────────────────────────────────────────────────────────
10
- // Template · Cross-check — the agent compares the documents of one transaction
11
- // and surfaces every field whose value DISAGREES across sources. Each card lays
12
- // the conflicting values side by side (with where each came from), marks the one
13
- // the agent believes, and the human resolves to a truth or flags it. The audit /
14
- // 3-way-match / contract-vs-invoice desk. Symmetric (N sources disagree), unlike
15
- // a before→after edit. The agreeing fields stay out of the way — only conflicts
16
- // need a decision.
17
- // ─────────────────────────────────────────────────────────────────────────────
18
-
19
- interface D {
20
- id: string;
21
- field: string;
22
- values: DiscrepancyValue[];
23
- note: string;
24
- }
25
-
26
- const SEED: D[] = [
27
- {
28
- id: "d1",
29
- field: "Total cartons",
30
- values: [
31
- { source: "Shipping instruction", value: "525" },
32
- { source: "Commercial invoice", value: "520", recommended: true },
33
- { source: "Packing list", value: "520" },
34
- ],
35
- note: "Two of three documents agree at 520; the SI looks stale — it predates the final load.",
36
- },
37
- {
38
- id: "d2",
39
- field: "Net weight",
40
- values: [
41
- { source: "Commercial invoice", value: "11,980 kg" },
42
- { source: "Packing list", value: "12,050 kg", recommended: true },
43
- ],
44
- note: "The packing list carries the measured weight; the CI rounded for the invoice.",
45
- },
46
- {
47
- id: "d3",
48
- field: "Incoterm",
49
- values: [
50
- { source: "Shipping instruction", value: "FOB" },
51
- { source: "Commercial invoice", value: "CIF", recommended: true },
52
- ],
53
- note: "The commercial invoice governs the sale terms — CIF matches the freight prepaid on the bill of lading.",
54
- },
55
- {
56
- id: "d4",
57
- field: "HS code",
58
- values: [
59
- { source: "Commercial invoice", value: "0306.17", recommended: true },
60
- { source: "Customs draft", value: "0306.16" },
61
- ],
62
- note: "0306.16 is cold-water shrimp; the goods are warm-water Penaeidae → 0306.17. The CI is right — fix the draft.",
63
- },
64
- ];
65
-
66
- const CHECKED = 22;
67
-
68
- export function TplCrosscheck() {
69
- const [res, setRes] = useState<Record<string, { idx?: number; flagged?: boolean }>>({});
70
-
71
- const resolvedCount = Object.values(res).filter((r) => r.idx != null || r.flagged).length;
72
- const openCount = SEED.length - resolvedCount;
73
-
74
- return (
75
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
76
- <View style={{ width: "100%", maxWidth: 760, alignSelf: "center", gap: 16 }}>
77
- <View style={{ gap: 2 }}>
78
- <Text size="xl" weight="semibold">Document cross-check</Text>
79
- <Text size="sm" color="muted">Shipment HBL-4471 · Shipping instruction · Commercial invoice · Packing list · Customs draft</Text>
80
- </View>
81
-
82
- <KPIStrip
83
- items={[
84
- { label: "Fields checked", value: CHECKED, format: "number" },
85
- { label: "In agreement", value: CHECKED - SEED.length, format: "number" },
86
- { label: "Disagree", value: openCount, format: "number", tone: openCount > 0 ? "danger" : "default", info: "Fields whose value differs across the documents — each needs a resolution before the set can be filed." },
87
- { label: "Resolved", value: resolvedCount, format: "number" },
88
- ]}
89
- />
90
-
91
- {openCount > 0 ? (
92
- <Callout tone="warning">
93
- <CalloutTitle>{openCount} of {CHECKED} fields disagree across the documents</CalloutTitle>
94
- <CalloutText>The agent reconciled the other {CHECKED - SEED.length}. Resolve each conflict to a source of truth, or flag it for the desk.</CalloutText>
95
- </Callout>
96
- ) : (
97
- <Callout tone="success">
98
- <CalloutTitle>All conflicts resolved</CalloutTitle>
99
- <CalloutText>Every field now has a single source of truth — the document set is ready to file.</CalloutText>
100
- </Callout>
101
- )}
102
-
103
- <View style={{ gap: 10 }}>
104
- {SEED.map((d) => (
105
- <Discrepancy
106
- key={d.id}
107
- field={d.field}
108
- values={d.values}
109
- note={d.note}
110
- resolvedIndex={res[d.id]?.idx}
111
- flagged={res[d.id]?.flagged}
112
- onResolve={(idx) => setRes((p) => ({ ...p, [d.id]: { idx } }))}
113
- onFlag={() => setRes((p) => ({ ...p, [d.id]: { flagged: true } }))}
114
- />
115
- ))}
116
- </View>
117
- </View>
118
- </ScrollView>
119
- );
120
- }
@@ -1,163 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { ScrollView, View } from "react-native";
3
- import { colors } from "@lotics/ui/colors";
4
- import { Text } from "@lotics/ui/text";
5
- import { Button } from "@lotics/ui/button";
6
- import { Card, CardBody, CardFooter, CardHeader, CardHeaderTitle } from "@lotics/ui/card";
7
- import { SegmentedControl } from "@lotics/ui/segmented_control";
8
- import { TextInputField } from "@lotics/ui/text_input_field";
9
- import { DotsIndicator } from "@lotics/ui/dots_indicator";
10
- import { CompletionState } from "@lotics/ui/completion_state";
11
-
12
- // ─────────────────────────────────────────────────────────────────────────────
13
- // Template · AI draft generation — a FOURTH AI workflow shape: the agent writes
14
- // a first DRAFT from context, and the human refines it. The feedback differs
15
- // from the others — the OUTPUT itself streams (the reply text fills in live),
16
- // then becomes an editable surface the person edits before sending. Tone +
17
- // length steer the generation; Regenerate re-drafts. AI drafts, the human owns
18
- // the final words. All mock — the recipe, not a model.
19
- // ─────────────────────────────────────────────────────────────────────────────
20
-
21
- type Tone = "friendly" | "formal";
22
- type Length = "short" | "detailed";
23
-
24
- const INBOUND =
25
- "Hi — we're launching a new product line and need about 5,000 corrugated mailers " +
26
- "(FEFCO 0427) per month. Could you share pricing and a lead time?";
27
-
28
- const OPENING: Record<Tone, string> = {
29
- friendly: "Hi Maya,\n\nThanks for reaching out — exciting to hear about the new line!",
30
- formal: "Dear Ms. Tran,\n\nThank you for your enquiry regarding corrugated mailers.",
31
- };
32
- const OFFER: Record<Length, string> = {
33
- short:
34
- "For 5,000 FEFCO 0427 mailers a month we can offer 8,200₫/unit at that volume, with a 7–10 day lead time after artwork sign-off.",
35
- detailed:
36
- "For 5,000 FEFCO 0427 mailers a month, our volume price is 8,200₫/unit (B-flute, 1-colour print). Lead time is 7–10 working days after artwork sign-off, and the first run includes the cutting die at no charge. We can hold this pricing for 30 days, and step it down again past 10,000 units a month.",
37
- };
38
- const CLOSE: Record<Tone, string> = {
39
- friendly: "Happy to send samples whenever you're ready — just say the word.\n\nBest,\nLan",
40
- formal: "We would be glad to provide samples upon request.\n\nKind regards,\nLan Nguyen\nSunrise Packaging",
41
- };
42
- const draftFor = (tone: Tone, length: Length) =>
43
- `${OPENING[tone]}\n\n${OFFER[length]}\n\n${CLOSE[tone]}`;
44
-
45
- export function TplDraft() {
46
- const [phase, setPhase] = useState<"idle" | "drafting" | "ready" | "sent">("idle");
47
- const [tone, setTone] = useState<Tone>("friendly");
48
- const [length, setLength] = useState<Length>("short");
49
- const [draft, setDraft] = useState("");
50
- const [target, setTarget] = useState("");
51
- const [revealed, setRevealed] = useState(0);
52
-
53
- // Stream the draft text in, word by word, then hand it to the editor.
54
- useEffect(() => {
55
- if (phase !== "drafting") return;
56
- const words = target.split(" ");
57
- if (revealed >= words.length) {
58
- setDraft(target);
59
- setPhase("ready");
60
- return;
61
- }
62
- const t = setTimeout(() => setRevealed((r) => r + 2), revealed === 0 ? 200 : 45);
63
- return () => clearTimeout(t);
64
- }, [phase, revealed, target]);
65
-
66
- const startDraft = () => {
67
- setTarget(draftFor(tone, length));
68
- setRevealed(0);
69
- setDraft("");
70
- setPhase("drafting");
71
- };
72
-
73
- const streamed = phase === "drafting" ? target.split(" ").slice(0, revealed).join(" ") : "";
74
-
75
- return (
76
- <ScrollView style={{ backgroundColor: colors.white }} contentContainerStyle={{ padding: 28, paddingBottom: 80 }}>
77
- <View style={{ maxWidth: 720, width: "100%", alignSelf: "center", gap: 16 }}>
78
- <View style={{ gap: 2 }}>
79
- <Text size="xl" weight="semibold">Reply to inquiry</Text>
80
- <Text size="sm" color="muted">The agent drafts a reply from the message; you set the tone, then edit before sending.</Text>
81
- </View>
82
-
83
- <Card style={{ padding: 0 }}>
84
- <CardHeader><CardHeaderTitle>Inbound</CardHeaderTitle></CardHeader>
85
- <CardBody>
86
- <View style={{ flexDirection: "row", gap: 10 }}>
87
- <View style={{ width: 28, height: 28, borderRadius: 14, backgroundColor: colors.zinc[200], alignItems: "center", justifyContent: "center" }}>
88
- <Text size="xs" weight="semibold" color="muted">MT</Text>
89
- </View>
90
- <Text size="sm" style={{ flex: 1 }}>{INBOUND}</Text>
91
- </View>
92
- </CardBody>
93
- </Card>
94
-
95
- {phase === "sent" ? (
96
- <Card>
97
- <CompletionState title="Reply sent" summary="Your edited draft was sent to Maya Tran.">
98
- <Button title="Draft another" color="secondary" onPress={() => { setPhase("idle"); setDraft(""); }} />
99
- </CompletionState>
100
- </Card>
101
- ) : (
102
- <Card style={{ padding: 0 }}>
103
- <CardHeader>
104
- <CardHeaderTitle info="Tone and length steer the draft; you edit the result before it goes out.">Your reply</CardHeaderTitle>
105
- </CardHeader>
106
- <CardBody>
107
- <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 16 }}>
108
- <View style={{ gap: 6 }}>
109
- <Text size="xs" color="muted" weight="medium">Tone</Text>
110
- <SegmentedControl
111
- accessibilityLabel="Tone"
112
- options={[{ label: "Friendly", value: "friendly" }, { label: "Formal", value: "formal" }]}
113
- value={tone}
114
- onValueChange={setTone}
115
- disabled={phase === "drafting"}
116
- />
117
- </View>
118
- <View style={{ gap: 6 }}>
119
- <Text size="xs" color="muted" weight="medium">Length</Text>
120
- <SegmentedControl
121
- accessibilityLabel="Length"
122
- options={[{ label: "Short", value: "short" }, { label: "Detailed", value: "detailed" }]}
123
- value={length}
124
- onValueChange={setLength}
125
- disabled={phase === "drafting"}
126
- />
127
- </View>
128
- </View>
129
-
130
- {phase === "idle" ? (
131
- <View style={{ alignItems: "flex-start", paddingTop: 4 }}>
132
- <Button title="Draft a reply" color="primary" onPress={startDraft} />
133
- </View>
134
- ) : null}
135
-
136
- {phase === "drafting" ? (
137
- <View style={{ borderWidth: 1, borderColor: colors.zinc[200], borderRadius: 10, padding: 14, gap: 10, backgroundColor: colors.white }}>
138
- <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
139
- <Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Drafting</Text>
140
- <DotsIndicator size={5} color={colors.zinc[900]} />
141
- </View>
142
- <Text size="sm">{streamed}</Text>
143
- </View>
144
- ) : null}
145
-
146
- {phase === "ready" ? (
147
- <TextInputField value={draft} onChangeText={setDraft} multiline numberOfLines={12} autoGrow />
148
- ) : null}
149
- </CardBody>
150
-
151
- {phase === "ready" ? (
152
- <CardFooter>
153
- <View style={{ flex: 1 }} />
154
- <Button title="Regenerate" color="secondary" icon="rotate-ccw" onPress={startDraft} />
155
- <Button title="Send reply" color="primary" onPress={() => setPhase("sent")} />
156
- </CardFooter>
157
- ) : null}
158
- </Card>
159
- )}
160
- </View>
161
- </ScrollView>
162
- );
163
- }